mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(native-chat): keep Claude Fable selectable when CLI probe omits it
The Claude CLI's list_models control request does not advertise Fable until the user has completed its one-time usage-credit consent. Because the native-chat enrichment replaced the seed model list with the probe result for Claude, Fable vanished from the model picker on any host whose CLI had not yet listed it. Switch Claude to the same additive merge used by other non-authoritative agents: seed rows (fable, opus, sonnet, haiku) are retained, and discovered rows overlay matching ids and append new ones. For matching ids the discovered option menus (effort levels, fast mode) win when present, so per-host capabilities still drive the picker. Also add Fable to the commit-message Claude agent's fallback model list so it is available for non-interactive generation too.
This commit is contained in:
+5
-2
@@ -98,7 +98,7 @@ describe('native chat session option enrichment', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('uses only discovered Claude rows and capabilities per host', async () => {
|
||||
it('merges seed Claude rows with discovered capabilities per host', async () => {
|
||||
mocks.discoverRuntimeCommitMessageModels.mockResolvedValue({
|
||||
success: true,
|
||||
catalogOrigin: 'probe',
|
||||
@@ -135,7 +135,10 @@ describe('native chat session option enrichment', () => {
|
||||
await vi.waitFor(() => expect(listener).toHaveBeenCalledOnce())
|
||||
|
||||
const models = readNativeChatEnrichedModels('claude', 'ssh:host')!
|
||||
expect(models.map(({ id }) => id)).toEqual(['opus[1m]', 'sonnet'])
|
||||
// Why: seed rows the CLI omits (e.g. fable, which needs a one-time consent
|
||||
// before list_models advertises it) must stay selectable, so the merge keeps
|
||||
// every seed id and overlays discovered rows on top.
|
||||
expect(models.map(({ id }) => id)).toEqual(['fable', 'opus', 'sonnet', 'haiku', 'opus[1m]'])
|
||||
const sonnetEffort = models.find(({ id }) => id === 'sonnet')?.options[0]
|
||||
expect(sonnetEffort?.kind).toMatchObject({
|
||||
type: 'select',
|
||||
|
||||
@@ -101,12 +101,9 @@ export function ensureNativeChatModelEnrichment(args: {
|
||||
if (!discovered || discovered.length === 0) {
|
||||
return
|
||||
}
|
||||
entry.models =
|
||||
args.agent === 'claude'
|
||||
? [...discovered]
|
||||
: catalog.discoveredModelsAreAuthoritative
|
||||
? mergeDiscoveredAuthoritativeModels(catalog.models, discovered)
|
||||
: mergeCatalogModels(catalog.models, discovered)
|
||||
entry.models = catalog.discoveredModelsAreAuthoritative
|
||||
? mergeDiscoveredAuthoritativeModels(catalog.models, discovered)
|
||||
: mergeCatalogModels(catalog.models, discovered)
|
||||
for (const listener of entry.listeners) {
|
||||
listener([...entry.models])
|
||||
}
|
||||
|
||||
@@ -82,10 +82,14 @@ describe('useNativeChatSessionOptions model reporting', () => {
|
||||
await waitFor(() =>
|
||||
expect(modelDescriptor(result.current.snapshot).currentValue).toBe('opus[1m]')
|
||||
)
|
||||
// The invented family row is gone: every choice is one the host listed.
|
||||
// Seed rows the host omitted (e.g. fable) stay selectable; discovered rows
|
||||
// overlay the matching seed ids and append any new ones.
|
||||
expect(modelDescriptor(result.current.snapshot).choices.map((choice) => choice.value)).toEqual([
|
||||
'opus[1m]',
|
||||
'haiku'
|
||||
'fable',
|
||||
'opus',
|
||||
'sonnet',
|
||||
'haiku',
|
||||
'opus[1m]'
|
||||
])
|
||||
})
|
||||
|
||||
@@ -130,7 +134,7 @@ describe('useNativeChatSessionOptions model reporting', () => {
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
modelDescriptor(result.current.snapshot).choices.map((choice) => choice.value)
|
||||
).toEqual(['opus[1m]', 'haiku'])
|
||||
).toEqual(['fable', 'opus', 'sonnet', 'haiku', 'opus[1m]'])
|
||||
)
|
||||
expect(modelDescriptor(result.current.snapshot).currentValue).toBeUndefined()
|
||||
})
|
||||
@@ -171,7 +175,7 @@ describe('useNativeChatSessionOptions model reporting', () => {
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
modelDescriptor(result.current.snapshot).choices.map((choice) => choice.value)
|
||||
).toEqual(['opus[1m]', 'haiku'])
|
||||
).toEqual(['fable', 'opus', 'sonnet', 'haiku', 'opus[1m]'])
|
||||
)
|
||||
expect(modelDescriptor(result.current.snapshot).currentValue).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -54,7 +54,11 @@ export function findCatalogOption(
|
||||
return model?.options.find((option) => option.id === optionId)
|
||||
}
|
||||
|
||||
/** Merge live rows over the static seed while retaining cataloged option mappings. */
|
||||
/** Merge live rows over the static seed while retaining cataloged option mappings.
|
||||
* Why: for agents whose discovered rows carry their own option menus (Claude's
|
||||
* per-model effort levels and fast-mode flag come from the CLI probe), the live
|
||||
* options win; for agents whose discovery returns no options, the seed's menus
|
||||
* are kept so the picker does not go blank. */
|
||||
export function mergeCatalogModels(
|
||||
seed: readonly CatalogModel[],
|
||||
discovered: readonly CatalogModel[]
|
||||
@@ -66,7 +70,7 @@ export function mergeCatalogModels(
|
||||
return model
|
||||
}
|
||||
discoveredById.delete(model.id)
|
||||
return { ...model, ...live, options: model.options }
|
||||
return { ...model, ...live, options: live.options.length > 0 ? live.options : model.options }
|
||||
})
|
||||
return [...merged, ...discoveredById.values()]
|
||||
}
|
||||
|
||||
@@ -357,6 +357,12 @@ export const COMMIT_MESSAGE_AGENT_SPECS: Partial<Record<TuiAgent, CommitMessageA
|
||||
{
|
||||
// Why: Claude Code aliases track the account/provider's supported
|
||||
// model IDs; hardcoded version IDs can be rejected by Bedrock/Vertex.
|
||||
id: 'fable',
|
||||
label: 'Fable',
|
||||
thinkingLevels: CLAUDE_THINKING_LEVELS,
|
||||
defaultThinkingLevel: 'high'
|
||||
},
|
||||
{
|
||||
id: 'haiku',
|
||||
label: 'Haiku'
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user