Skip to content

UN-3991 [FIX] Show model names on Prompt Studio tiles for shared users - #2240

Open
kirtimanmishrazipstack wants to merge 3 commits into
mainfrom
UN-3991-shared-project-model-names
Open

UN-3991 [FIX] Show model names on Prompt Studio tiles for shared users#2240
kirtimanmishrazipstack wants to merge 3 commits into
mainfrom
UN-3991-shared-project-model-names

Conversation

@kirtimanmishrazipstack

@kirtimanmishrazipstack kirtimanmishrazipstack commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What

  • Prompt Studio output tiles now show the model name and icon for anyone who can open the project, not just its owner.
  • The backend sends those display details along with each LLM profile, so the UI no longer has to look them up on its own.

Why

  • A user the project was shared with — directly or through a group — saw a blank model name and no icon on the tiles.
  • The UI built those labels from the list of adapters that user personally has access to, and a shared project's adapters are not in it.

How

  • The profile serializer resolves the four adapter FKs at render time and returns conf (per-adapter model label) and icon — display data only, no credentials.
  • get_queryset adds select_related on those four FKs so the extra reads don't become N+1.
  • PromptCardItems.jsx renders the payload as-is; the frontend adapter lookup is gone.

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • No. The payload only gains conf/icon; the adapter fields keep returning the adapter name as before. AdapterInstance.objects.for_user is untouched — this changes what is displayed, not what a user can reach or use.

Database Migrations

  • None

Env Config

  • None

Relevant Docs

Related Issues or PRs

  • Zipstack/unstract-cloud#1722 — agentic Prompt Studio, same ticket

Dependencies Versions

  • None

Notes on Testing

  • backend/prompt_studio/prompt_profile_manager_v2/tests/test_profile_display_info.py — covers adapter FK resolution with no adapter access.
  • Manual: share a Prompt Studio project that uses a private LLM (once directly, once via a group), open it as the invited user — the tiles show the model name.

Screenshots

11
12

Checklist

I have read and understood the Contribution Guidelines.

The output tiles resolved the model label by matching the profile against
the access-filtered adapter list, so a project shared without its adapters
rendered no icon and no model name. The profile payload now carries the
display data directly, and the frontend renders it as-is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR moves Prompt Studio profile display metadata resolution to the backend so shared users can see model names and icons without direct adapter access.

  • Adds model labels and the LLM icon to serialized profile payloads while preloading adapter relationships.
  • Renders image-backed icons as images and warning fallbacks as text in both output views.
  • Removes the frontend’s viewer-scoped adapter lookup and adds serializer coverage for shared profiles.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the missing registry-icon fallback is preserved and the warning value is now rendered as text rather than passed to an image source in both affected output views.

Important Files Changed

Filename Overview
backend/adapter_processor_v2/adapter_processor.py Adds resilient adapter display metadata resolution, including the missing-icon fallback required by the earlier review.
backend/prompt_studio/prompt_profile_manager_v2/serializers.py Resolves adapter names, model labels, and the LLM icon directly from profile relationships without applying viewer adapter access filtering.
backend/prompt_studio/prompt_profile_manager_v2/views.py Preloads all adapter relationships consumed by profile serialization to avoid per-profile queries.
frontend/src/components/custom-tools/prompt-card/PromptCardItems.jsx Consumes backend-provided profile display metadata instead of rebuilding it from the current viewer’s adapter list.
frontend/src/components/custom-tools/prompt-card/PromptOutput.jsx Distinguishes image URLs from textual fallback icons in the primary output tile.
frontend/src/components/custom-tools/prompt-card/PromptOutputsModal.jsx Applies the same corrected icon rendering behavior in the output modal.
frontend/src/helpers/GetStaticData.js Adds the shared URL classifier used to prevent warning emoji from being passed to an image source.
backend/prompt_studio/prompt_profile_manager_v2/tests/test_profile_display_info.py Covers profile display metadata resolution with configured, partially configured, and empty adapter sets.

Sequence Diagram

sequenceDiagram
    participant Viewer as Shared user
    participant UI as Prompt Studio UI
    participant API as Profile API
    participant Serializer as Profile serializer
    participant Adapter as Adapter instance
    Viewer->>UI: Open shared project
    UI->>API: Request profiles
    API->>Serializer: Serialize authorized profiles
    Serializer->>Adapter: Resolve name, model, and icon
    Adapter-->>Serializer: Display-only metadata
    Serializer-->>UI: Profile with conf and icon
    UI-->>Viewer: Render model name and image or warning icon
Loading

Fix All in Greploop

Reviews (3): Last reviewed commit: "UN-3991 [FIX] Render the adapter icon fa..." | Re-trigger Greptile

Comment thread backend/adapter_processor_v2/adapter_processor.py Outdated
get_adapter_data_with_key returns the registry entry's value, so an
adapter whose entry has no icon key yields None without raising. The
except branch never fired and None replaced the fallback icon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread backend/adapter_processor_v2/adapter_processor.py
The icon is an emoji when the adapter is unavailable or has no registry
icon, but the Prompt Studio output tiles passed it to antd Image as src,
so it loaded as a URL and rendered a broken image.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Frontend Lint Report (Biome)

All checks passed! No linting or formatting issues found.

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
e2e-api-deployment e2e 3 0 0 0 21.0
e2e-coowners e2e 1 0 0 0 1.3
e2e-etl e2e 1 0 0 0 8.2
e2e-login e2e 2 0 0 0 1.1
e2e-prompt-studio e2e 1 0 0 0 4.5
e2e-smoke e2e 2 0 0 0 1.3
e2e-workflow e2e 1 0 0 0 16.6
integration-backend integration 267 0 0 26 46.3
integration-connectors integration 1 0 0 7 8.0
integration-workers integration 140 0 0 1 49.3
unit-backend unit 1001 0 0 1 32.2
unit-connectors unit 63 0 0 0 9.6
unit-core unit 33 0 0 0 1.1
unit-platform-service unit 15 0 0 0 2.4
unit-rig unit 117 0 0 0 4.5
unit-sdk1 unit 480 0 0 0 19.7
unit-workers unit 1335 0 0 1 82.0
TOTAL 3463 0 0 36 309.0

Critical paths

⚠️ Critical paths not yet covered

  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (declared coverage: no groups declared)
✅ Covered critical paths
  • auth-login — covered by e2e-login
  • adapter-register-llm — covered by integration-backend
  • workflow-author — covered by integration-backend
  • co-owner-manage — covered by integration-backend, e2e-coowners
  • workflow-create-execute — covered by e2e-workflow
  • api-deployment-provision — covered by integration-backend
  • api-deployment-auth — covered by integration-backend
  • api-deployment-run — covered by e2e-api-deployment
  • mcp-server-auth — covered by integration-backend
  • mcp-platform-auth — covered by integration-backend
  • prompt-studio-author — covered by integration-backend
  • prompt-studio-fetch-response — covered by e2e-prompt-studio
  • connector-register-test — covered by integration-backend
  • pipeline-etl-execute — covered by e2e-etl
  • usage-aggregate-read — covered by integration-backend
  • usage-token-tracking — covered by e2e-api-deployment
  • callback-result-delivery — covered by e2e-api-deployment

@vishnuszipstack vishnuszipstack left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review — PR Review Toolkit (6 agents)

Ran code review, silent-failure, type-design, test-coverage, comment-accuracy and simplification passes over the 9 changed files, then verified each finding against the actual code before posting. Skipped anything already covered in the existing adapter_processor.py:127/:130 threads (the or UNAVAILABLE_ADAPTER_ICON guard and the emoji-as-<img src> fix) — both look correctly resolved.

The core approach is right. Moving model/icon resolution server-side is the correct fix for a shared-project viewer: no client-side join can produce a name for an adapter the viewer can't list. select_related is the right instinct, the frontend cleanup is complete (adapters came from the store, not props — no stale PropTypes or dead prop-drilling left behind), and deleting get_adapter_instance_by_id is safe (zero references repo-wide, and the Adapter import it used is still needed at line 192).

Three findings are worth blocking on:

1. The select_related is on a queryset that never lists. ProfileManagerView has no list route — prompt_profile_manager_v2/urls.py wires only <uuid:pk>/. The endpoint that actually loads llmProfiles is PromptStudioCoreView.list_profiles (prompt_studio_core_v2/views.py:376), which has no select_related and uses the same serializer with many=True. The N+1 is unmitigated on the one path the feature exists for.

2. Each icon lookup rescans the whole SDK registry from disk, and 3 of 4 results are discarded. get_adapter_data_with_keyAdapterkit().get_adapters_list(), which is not memoised and calls get_json_schema() — an open().read() — for all 37 registered adapters. get_display_info resolves the icon for every adapter but the serializer keeps only the LLM's. A 4-profile project is ~590 file opens and 16 INFO log lines per profile-list request, ~75% of it wasted. Splitting get_display_info into get_icon() / get_model_label() fixes this and the positional-tuple hazard in one move.

3. The fix is partial. getLLMModelNamesForProfiles (GetStaticData.js:499) is the same client-side join just deleted from PromptCardItems.jsx, still live at CombinedOutput.jsx:151 and OutputForDocModal.jsx:186. Both fetch the viewer's /adapter/?adapter_type=LLM list, so the Combined Output view and the per-document output modal still show a blank model name for shared users. Fine to defer, but the PR description currently reads as though the bug is closed.

Beyond those: InvalidEncryptionKey — a documented platform-wide outage — is swallowed into a logger.warning on the one path a shared viewer can see, and the new test mocks out get_display_info, so both of the bug fixes in commits b30385de and ca031c15 have zero coverage. Details inline.

Two notes on things I checked that are not problems: the timeAgo export move is Biome's organizeImports re-sorting the block, not noise — reverting it would fail lint:changed. And conf["Profile Name"] being gated on conf being non-empty faithfully reproduces the old frontend behaviour (the assignment sat inside if (adapter) on main).

One pre-existing quirk worth a comment rather than a change: serializers.py:40 replaces the FK UUID with adapter_name, so a GET response can't be PUT back. That's load-bearing — AddLlmProfile.jsx:129 matches the edit form by item?.label === llmProfileDetails?.llm — and predates this PR. Worth a line so nobody "fixes" it later.

def get_queryset(self) -> QuerySet | None:
queryset = ProfileManager.objects.for_user(self.request.user)
# Serializer reads all four adapters per profile for the display info
queryset = ProfileManager.objects.for_user(self.request.user).select_related(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — the select_related landed on a queryset that never serializes more than one profile.

ProfileManagerView has no list route. prompt_profile_manager_v2/urls.py wires only profile-manager/<uuid:pk>/retrieve/update/partial_update/destroy, so this queryset only ever yields a single profile and the join saves at most 3 queries.

The endpoint that actually populates llmProfiles — the one this feature exists for — is GET prompt-studio/prompt-studio-profile/<pk>/ (frontend/src/components/helpers/custom-tools/CustomToolsHelper.js:102prompt_studio_core_v2/urls.py:96), which lands on PromptStudioCoreView.list_profiles at backend/prompt_studio/prompt_studio_core_v2/views.py:376:

profile_manager_instances = ProfileManager.objects.filter(
    prompt_studio_tool=prompt_tool
)
serialized_instances = ProfileManagerSerializer(
    profile_manager_instances, many=True
).data

No select_related, same serializer, many=True. Since to_representation now dereferences all four FKs per row, that path does 4 lazy FK queries per profile — the exact N+1 this change was meant to prevent, on the hot path, unmitigated.

Suggested fix — add the same join in list_profiles:

profile_manager_instances = ProfileManager.objects.filter(
    prompt_studio_tool=prompt_tool
).select_related("llm", "embedding_model", "vector_store", "x2text")

Worth keeping the join here too (it still helps retrieve), but the comment above it implies it protects a list read that this view doesn't serve.

adapter = getattr(instance, field, None)
if not adapter:
continue
icon, model = AdapterProcessor.get_display_info(adapter)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — the icon is resolved for all four adapters and kept for one, and each resolution rescans the entire SDK registry from disk.

rep["icon"] is only set in the llm branch below, but get_display_info resolves the icon unconditionally. The discarded work is not cheap:

get_display_infoget_adapter_data_with_key__fetch_adapters_by_key_value (adapter_processor.py:196-199) → Adapterkit().get_adapters_list().

Adapterkit is a @singleton, but get_adapters_list() is not memoised (unstract/sdk1/src/unstract/sdk1/adapters/adapterkit.py:64-86) — it loops every registered adapter and calls m.get_json_schema(), which is a file read:

with open(schema_path) as f:
    return f.read()

There are 37 adapter schema files in the registry. So each icon lookup is a full registry scan with ~37 open() calls, plus an INFO log line (adapter_processor.py:196). For a 4-profile project that is 16 lookups ≈ 590 file opens and 16 INFO lines per profile-list request — and 12 of those 16 lookups have their result thrown away.

Two independent fixes, both small:

  1. Only resolve the icon where it's used:
for field, label in ADAPTER_LABELS:
    adapter = getattr(instance, field)
    conf[label] = AdapterProcessor.get_model_label(adapter)
    rep[field] = adapter.adapter_name
if instance.llm_id:
    rep["icon"] = AdapterProcessor.get_icon(instance.llm)
  1. Make the icon lookup O(1) — Adapterkit already exposes a direct dict route that avoids get_adapters_list() entirely:
icon = Adapterkit().get_adapter_class_by_adapter_id(adapter.adapter_id).get_icon()

Same underlying get_icon() value, no registry scan, no file I/O. It raises RuntimeError instead of InValidAdapterId for an unknown id, which the existing except Exception already covers.

// access to the project's adapters.
setLlmProfileDetails(
updatedProfiles
(llmProfiles || [])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — the fix is partial; two other output surfaces still resolve model names from the viewer's own adapter list.

Removing the client-side lookup here is right, but the identical pattern survives in two places, both routed through getLLMModelNamesForProfiles (frontend/src/helpers/GetStaticData.js:499-514), which maps profile.llmadapter.model using the caller's adapter list:

  • frontend/src/components/custom-tools/combined-output/CombinedOutput.jsx:145-151
  • frontend/src/components/custom-tools/output-for-doc-modal/OutputForDocModal.jsx:178-186

Both fetch /api/v1/unstract/{org}/adapter/?adapter_type=LLM, which returns only the adapters the viewer owns — empty for a shared-project user. So the Combined Output view and the per-document output modal still show a blank model name for exactly the users this PR targets.

Now that the backend ships conf.LLM on every profile, both call sites can read profile?.conf?.LLM directly, which also deletes two adapter API round-trips and lets getLLMModelNamesForProfiles go away.

If that's intentionally out of scope, worth saying so in the PR description — the "Can this PR break any existing features" section currently reads as though the fix is complete.

logger.warning(f"No icon for adapter {adapter.adapter_id}: {e}")
try:
model = adapter.metadata.get("model")
except Exception as e:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — this except Exception swallows InvalidEncryptionKey, which the codebase treats as a call-the-admin outage.

adapter.metadata is not a plain attribute — it's a decrypting property (adapter_processor_v2/models.py:190-201) that converts a Fernet InvalidToken into InvalidEncryptionKey. That exception is a 403 APIException whose detail reads "Platform encryption key for storing adapter credentials has changed! All adapters are inaccessible. Please inform the platform admin immediately." (backend/utils/exceptions.py:6-12).

Caught here, it becomes a logger.warning and a tile that renders a plausible-looking "openai". Note the asymmetry: AdapterListSerializer does the same instance.metadata.get("model") unguarded at adapter_processor_v2/serializers.py:206, so the adapters page surfaces the actionable 403 while the profile payload silently degrades — and for a shared-project viewer the profile payload is the only path, so they get no signal at all.

The same handler also absorbs several unrelated failures, all reachable, none of which "No metadata for adapter" describes:

  • TypeError: cannot convert 'NoneType' object to bytesadapter_metadata_b is BinaryField(null=True) (models.py:89) and is only populated when metadata is truthy (serializers.py:73-80).
  • ValueError from Fernet(...) on a malformed ENCRYPTION_KEY.
  • json.JSONDecodeError on corrupt plaintext.

Suggested fix — let the outage propagate, narrow the rest, and log at error:

try:
    model = adapter.metadata.get("model")
except InvalidEncryptionKey:
    raise  # platform-wide credential outage; must not degrade to a log line
except (TypeError, ValueError, AttributeError) as e:
    logger.error(
        "Unreadable metadata for adapter %s (%s): %s",
        adapter.id, adapter.adapter_id, e,
    )
    model = None

If a 403 on the profile list is considered too disruptive for a display path, that's a defensible call — but then it should be a deliberate one, with the log at error and a message that names the real condition rather than "No metadata".

)
conf: dict[str, str] = {}
for field, label in ADAPTER_LABELS:
adapter = getattr(instance, field, None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — getattr(instance, field, None) silently swallows a broken FK, and the continue branch is otherwise unreachable.

All four adapter FKs are null=False, blank=False, on_delete=models.PROTECT (prompt_profile_manager_v2/models.py:61-91). A None is therefore impossible under the schema — so if not adapter: continue can only fire in the one case that must not be silent.

Django's forward FK descriptor raises RelatedObjectDoesNotExist, which is constructed as a subclass of both model.DoesNotExist and AttributeError (django/db/models/fields/related_descriptors.py). Because it inherits AttributeError, the three-argument getattr catches it and returns the default. A dangling FK — raw SQL delete, partial restore, cross-schema drift — therefore produces no exception, no log, and no user-visible error: just a row quietly missing from the tooltip, and a blank icon and title if it's the llm FK.

Suggested fix — be explicit about the impossible case:

from django.core.exceptions import ObjectDoesNotExist

for field, label in ADAPTER_LABELS:
    try:
        adapter = getattr(instance, field)
    except ObjectDoesNotExist:
        logger.error(
            "Profile %s references a missing %s adapter (%s)",
            instance.profile_id, field, getattr(instance, f"{field}_id", None),
        )
        continue

ObjectDoesNotExist catches the real condition without the AttributeError blanket, so a genuine typo in ADAPTER_LABELS still fails loudly.

Related: test_unset_adapters_are_skipped and test_profile_with_no_adapters_has_empty_conf both assert on FK states the schema forbids, so neither covers this.

className="prompt-card-llm-icon"
/>
) : (
<span className="prompt-card-llm-icon">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 — when icon is absent this renders a zero-width empty span with no placeholder and nothing in the console.

isImageUrl(undefined) is false, so an undefined icon takes the <span> branch and renders nothing at all. Combined with {profile?.conf?.LLM} two lines down also being empty, the tile renders completely blank rather than indicating anything is wrong.

The backend always sets rep["icon"] when the llm FK resolves, so this isn't reachable today — but it becomes reachable via the dangling-FK path flagged on serializers.py:36, and via any stale cached profile payload predating this shape.

One-character fix, and it reuses the fallback the backend already picked:

<span className="prompt-card-llm-icon">{profile?.icon || "⚠️"}</span>

src={profile?.icon}
width={15}
height={15}
preview={false}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 — this 13-line block is byte-identical to PromptOutput.jsx:359-371 apart from indentation.

Worth extracting while it's only two copies — the width={15} height={15} preview={false} triple has to stay in sync with .prompt-card-llm-icon in PromptCard.css:150, which is exactly the kind of thing that drifts.

frontend/src/components/widgets/ isn't the right home (everything there is generic — space-wrapper, spinner-loader, empty-state); this is coupled to a prompt-card CSS class. But prompt-card/ already holds eight single-purpose leaf components (CopyPromptOutputBtn.jsx, ExpandCardBtn.jsx, PromptRunCost.jsx, …), so a new file there is conventional.

// prompt-card/ProfileIcon.jsx
function ProfileIcon({ icon }) {
  if (!isImageUrl(icon)) {
    return <span className="prompt-card-llm-icon">{icon || "⚠️"}</span>;
  }
  return (
    <Image src={icon} width={15} height={15} preview={false}
           className="prompt-card-llm-icon" />
  );
}

Both call sites collapse to <ProfileIcon icon={profile?.icon} />, both files drop their now-unused Image and isImageUrl imports, and the fallback above only has to be fixed once.

// Icons are image URLs, except the emoji fallbacks used for unavailable
// adapters. Detect the URL rather than the emoji - compound (ZWJ) emoji break
// length heuristics.
const isImageUrl = (value) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 — isImageUrl is the whole of commit ca031c1 and has no test, though the repo is set up for one.

The frontend has vitest 3.2.6 + happy-dom wired (frontend/vitest.config.mjs, "test": "vitest"), and there are already colocated helper tests in this exact directory — pagination.test.js and requestId.test.js. So this is a cheap, convention-following gap.

The implementation is correct for the real data: every get_icon() in the SDK registry returns a root-relative /icons/adapter-icons/*.png, which the ^\/ branch matches, and ⚠️ correctly falls through. But that correctness rests entirely on an implicit cross-service contract with the SDK, which is precisely what a regression test is for.

it.each([
  ["/icons/adapter-icons/OpenAI.png", true],
  ["https://cdn.example.com/x.png", true],
  ["data:image/png;base64,iVBOR", true],
  ["⚠️", false],        // must never regress into <img src>
  ["OpenAI.png", false],  // pins that a bare filename is not a URL
  [undefined, false], [null, false], [123, false],
])("isImageUrl(%p) === %p", (v, expected) => expect(isImageUrl(v)).toBe(expected));

The "⚠️" row is the valuable one — it prevents the emoji regressing back into an <img src>, which is the exact bug this commit fixed.

Caveat worth flagging separately: no workflow currently runs vitest (ci-frontend-lint.yaml only runs Biome, and there's no frontend group in tests/groups.yaml), so a test added here wouldn't be CI-gated today. That's a pre-existing infra gap, not a reason to skip the test.

Minor, on the comment above: "Icons are image URLs" — in production they're all root-relative paths; the https?:// and data:image/ branches match nothing today. And the emoji fallback fires not only for unavailable adapters but also when the SDK lookup fails or returns empty (adapter_processor.py:125-132), so "unavailable or unresolvable adapters" would be more accurate.

rep["icon"] = icon
if conf:
conf["Profile Name"] = instance.profile_name
rep["conf"] = conf

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 — conf is now a load-bearing wire contract with no schema and a client that can't tolerate its absence.

Two things worth pinning down:

1. The frontend dereferences conf unguarded. PromptOutput.jsx:132-140 does Object.entries(adapterConf)?.map(...), called with profile?.conf at line 381. Object.entries(undefined) throws — the ?. after the call is useless. Before this PR the client guaranteed conf existed because getModelOrAdapterId always returned { conf: {} } locally; now its provenance is entirely remote. The backend does always set it, so this isn't broken today, but the guard was removed at the same moment the value became remote, which is the wrong direction. Object.entries(adapterConf || {}) costs nothing.

2. Display labels are being used as payload keys. "LLM", "Embedding Model", "Vector Store", "Text Extractor" are human-readable strings, and "LLM" is indexed literally by the client at PromptOutput.jsx:374,376 and PromptOutputsModal.jsx:86 (profile?.conf?.LLM). The other three only reach a generic Object.entries tooltip, so they really are display text — but nothing distinguishes them. The comment on ADAPTER_LABELS at line 12 ("label shown on the Prompt Studio output tiles") actively invites someone to rename "LLM""Model" as a copy change, which would silently blank the tile title.

Also mixing "Profile Name" — a profile attribute, not an adapter — into the same flat namespace means the tooltip and the adapter set share one dict, so a future adapter type called "Profile Name" would collide.

Minimum: tighten the comment to say these are response keys and that "LLM" is read by key. Better, if you're willing: key conf by the FK name (llm, embedding_model, …) and let the frontend own the display strings, which is where they belong.

Small accuracy note: rep: dict[str, str] on line 33 is no longer true — conf is a nested dict, and super().to_representation() returns plenty of non-str values (UUIDs, ints, bools). dict[str, Any] is honest.

return updated_adapters[0].get(key_value)

@staticmethod
def get_display_info(adapter: AdapterInstance) -> tuple[str, str]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 (type design) — tuple[str, str] is the weakest available encoding for (icon, model), and splitting the function removes the hazard for free.

Both elements are str, so model, icon = AdapterProcessor.get_display_info(adapter) type-checks and silently ships an emoji as the model name. Only the docstring says which slot is which.

Normally I'd weigh that against a NamedTuple and conclude it isn't worth it — this repo has 34 bare -> tuple[...] returns against exactly one production NamedTuple, and adapter_processor_v2/ has zero dataclasses, so a 2-tuple is locally idiomatic and there's only one call site. But here a split is smaller than a NamedTuple and also fixes the wasted-work problem flagged on serializers.py:39:

@staticmethod
def get_model_label(adapter: AdapterInstance) -> str: ...  # metadata["model"] or provider prefix

@staticmethod
def get_icon(adapter: AdapterInstance) -> str: ...         # registry icon or UNAVAILABLE_ADAPTER_ICON

Two named single-value returns can't be swapped, need no new type, and let the caller resolve the icon only for the LLM — cutting registry walks from 4 per profile to 1.

Two smaller accuracy notes on the annotation itself:

  • str isn't actually enforced on either side. get_adapter_data_with_key returns Any (line 87) and .metadata returns Any (models.py:191), so both values are unchecked. It holds in practice (model is "type": "string" in all 37 adapter schemas), but mypy can't help — [tool.mypy] strict = true is set in pyproject.toml, yet the mypy pre-commit hook is commented out and no workflow invokes it.
  • The "always non-empty" invariant has one reachable hole: adapter_id is CharField(default="") (models.py:75-79), so "".split("|")[0] is "". An adapter with no model in metadata and an empty adapter_id returns ("⚠️", "") — satisfies str, renders as a blank tile, logs nothing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants