Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,13 @@ export function useModelHarness({
// different id namespaces; the provider is the reliable cross-harness signal on the config).
const selectedKeepsModel =
!modelId ||
harnessAllowsModel(capabilities, harnessValue, modelId) ||
harnessAllowsModel(
capabilities,
harnessValue,
modelId,
customSecrets,
connection.slug || null,
) ||
(!!connection.provider && selectedProviders.includes(connection.provider))
const selectedIsCurrent = !!harnessValue && (savedHarnessValue ?? harnessValue) === harnessValue
const selectedHarnessLabel =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,29 +364,57 @@ export function providerForModel(

/**
* Whether a model id is reachable under the harness (present in any of its published model
* groups). A harness with no published models is permissive (returns true) so the schema-catalog
* fallback path is not over-cleared. Use to clear an unreachable model on harness switch.
* groups or a reachable custom_provider vault connection). A harness with no published models is
* permissive (returns true) so the schema-catalog fallback path is not over-cleared. Use to clear
* an unreachable model on harness switch.
*/
export function harnessAllowsModel(
capabilities: HarnessCapabilitiesMap | null | undefined,
harness: string | null | undefined,
modelId: string | null | undefined,
customSecrets?: VaultModelSource[] | null | undefined,
slug?: string | null | undefined,
): boolean {
if (!modelId) return true
if (slug) {
if (customSecrets?.length) {
for (const secret of customSecrets) {
const secretSlug = secret.name?.trim()
const kind = secret.provider?.toLowerCase() || null
const secretModels = (secret.models ?? []).filter(Boolean)
if (!secretModels.includes(modelId)) continue
if (secretSlug !== slug) continue
if (!kind || harnessReachesCustomProviderKind(capabilities, harness, kind))
return true
}
}
return false
}

const caps = capsFor(capabilities, harness)
const catalog = caps?.model_catalog
const models = caps?.models
const hasCatalog = Boolean(catalog && catalog.length)
const hasModels = Boolean(models && Object.keys(models).length > 0)
// A harness with no published models at all is permissive (don't over-clear the schema-catalog
// fallback path).
if (!hasCatalog && !hasModels) return true
if (hasCatalog && catalog!.some((e) => e.id === modelId)) return true
if (
hasModels &&
Object.values(models!).some((ids) => Array.isArray(ids) && ids.includes(modelId))
)
return true

if (customSecrets?.length) {
for (const secret of customSecrets) {
const kind = secret.provider?.toLowerCase() || null
const secretModels = (secret.models ?? []).filter(Boolean)
if (!secretModels.includes(modelId)) continue
if (!kind || harnessReachesCustomProviderKind(capabilities, harness, kind)) return true
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

// A harness with no published models at all is permissive (don't over-clear the schema-catalog
// fallback path).
if (!hasCatalog && !hasModels) return true
return false
}

Expand Down
85 changes: 85 additions & 0 deletions web/packages/agenta-entity-ui/tests/unit/connectionUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,91 @@ describe("connectionUtils: harness-filtered model picker", () => {
expect(harnessAllowsModel(CAPABILITIES, "future-harness", "anything")).toBe(true)
expect(harnessAllowsModel(CAPABILITIES, "pi_core", null)).toBe(true)
})

it("supports custom-provider vault models with non-standard ID shapes when reachable by harness", () => {
const secrets = [
{
name: "my-bedrock",
provider: "bedrock",
models: ["custom-bedrock-model-id-123"],
},
]
// claude harness consumes bedrock -> returns true even with non-standard model ID shape
expect(
harnessAllowsModel(
CAPABILITIES,
"claude",
"custom-bedrock-model-id-123",
secrets,
"my-bedrock",
),
).toBe(true)
// pi_core harness does not consume bedrock -> returns false
expect(
harnessAllowsModel(
CAPABILITIES,
"pi_core",
"custom-bedrock-model-id-123",
secrets,
"my-bedrock",
),
).toBe(false)
// bogus model id not in secrets or catalog -> returns false
expect(
harnessAllowsModel(CAPABILITIES, "claude", "bogus-model-id", secrets, "my-bedrock"),
).toBe(false)
})

it("requires a specific vault connection to explicitly support a model when slug is provided, skipping generic catalog checks (name collision)", () => {
const secrets = [{name: "my-custom-conn", provider: "bedrock", models: ["other-model"]}]
// "opus" is in the claude catalog.
// A generic check (no slug) for "opus" returns true.
expect(harnessAllowsModel(CAPABILITIES, "claude", "opus")).toBe(true)

// But if we specifically ask whether "my-custom-conn" (which only supports "other-model")
// allows "opus", it must return false, not falling back to the catalog.
expect(harnessAllowsModel(CAPABILITIES, "claude", "opus", secrets, "my-custom-conn")).toBe(
false,
)

// And it should return true for the model it actually supports
expect(
harnessAllowsModel(CAPABILITIES, "claude", "other-model", secrets, "my-custom-conn"),
).toBe(true)
})

it("selectedKeepsModel regression: vault model flagged unavailable without secrets, available with them", () => {
// Reproduces the false 'model not available' badge: the selectedKeepsModel derivation in
// useModelHarness called harnessAllowsModel WITHOUT customSecrets or slug. The function is
// correct — the call site was wrong. This test locks that in.
const secrets = [
{name: "my-bedrock", provider: "bedrock", models: ["custom-bedrock-model-id-123"]},
]
// Old call (no secrets) — returns false → badge wrongly showed "model not available"
expect(harnessAllowsModel(CAPABILITIES, "claude", "custom-bedrock-model-id-123")).toBe(
false,
)
// Fixed call (secrets + slug threaded through) — returns true → badge shows "supports your model"
expect(
harnessAllowsModel(
CAPABILITIES,
"claude",
"custom-bedrock-model-id-123",
secrets,
"my-bedrock",
),
).toBe(true)
// Slug mismatch → still false (the credential is for a different connection)
expect(
harnessAllowsModel(
CAPABILITIES,
"claude",
"custom-bedrock-model-id-123",
secrets,
"other-connection",
),
).toBe(false)
})
})

describe("connectionUtils: model_catalog is preferred when published", () => {
Expand Down
Loading