diff --git a/frontend/src/pages/ChatView.tsx b/frontend/src/pages/ChatView.tsx
index 3d191e42..45a0a05c 100644
--- a/frontend/src/pages/ChatView.tsx
+++ b/frontend/src/pages/ChatView.tsx
@@ -223,9 +223,11 @@ export function ChatView() {
onChange={(e) => setModel(e.target.value)}
disabled={messages.running}
>
+
+
diff --git a/frontend/src/pages/DesktopChatView.tsx b/frontend/src/pages/DesktopChatView.tsx
index 1e85533b..a201ef1e 100644
--- a/frontend/src/pages/DesktopChatView.tsx
+++ b/frontend/src/pages/DesktopChatView.tsx
@@ -198,9 +198,11 @@ export function DesktopChatView() {
onChange={(e) => setModel(e.target.value)}
disabled={messages.running}
>
+
+
diff --git a/packages/harness/src/providers/anthropic-vertex.ts b/packages/harness/src/providers/anthropic-vertex.ts
index 17066abe..ef517961 100644
--- a/packages/harness/src/providers/anthropic-vertex.ts
+++ b/packages/harness/src/providers/anthropic-vertex.ts
@@ -16,6 +16,7 @@ const log = createLogger('provider:anthropic');
/** Vertex AI model name mapping (Vertex uses different naming). */
const VERTEX_MODEL_MAP: Record = {
'claude-opus-4-6': 'claude-opus-4-6@20250514',
+ 'claude-sonnet-5': 'claude-sonnet-5',
'claude-sonnet-4-6': 'claude-sonnet-4-6@20250514',
'claude-haiku-4-5': 'claude-3-5-haiku@20241022',
};
diff --git a/server/__tests__/model-spec.test.ts b/server/__tests__/model-spec.test.ts
index 095ec4e5..7473cd0b 100644
--- a/server/__tests__/model-spec.test.ts
+++ b/server/__tests__/model-spec.test.ts
@@ -62,4 +62,15 @@ describe('resolveThinking', () => {
it('returns undefined for haiku', () => {
expect(resolveThinking('claude-haiku-4-5')).toBeUndefined();
});
+
+ it('returns undefined for fable (always-on thinking, must not pass disabled)', () => {
+ expect(resolveThinking('claude-fable-5')).toBeUndefined();
+ });
+
+ it('returns 10k budget for sonnet 5', () => {
+ expect(resolveThinking('claude-sonnet-5')).toEqual({
+ type: 'enabled',
+ budgetTokens: 10_000,
+ });
+ });
});
diff --git a/server/chat.ts b/server/chat.ts
index 50579b77..3d668ae6 100644
--- a/server/chat.ts
+++ b/server/chat.ts
@@ -365,10 +365,14 @@ export function getRepoConfig() {
return _cachedConfig;
}
+export const FALLBACK_MODEL = 'claude-opus-4-6';
+
export const AVAILABLE_MODELS = [
+ { id: 'claude-fable-5', label: 'Fable 5', desc: 'Most capable (direct API only)' },
{ id: 'claude-opus-4-7', label: 'Opus 4.7', desc: 'Adaptive thinking' },
{ id: 'claude-opus-4-7:max', label: 'Opus 4.7 Max', desc: 'Max thinking (128k)' },
{ id: 'claude-opus-4-6', label: 'Opus 4.6', desc: 'Previous Opus' },
+ { id: 'claude-sonnet-5', label: 'Sonnet 5', desc: 'Latest Sonnet' },
{ id: 'claude-sonnet-4-6', label: 'Sonnet 4.6', desc: 'Balanced' },
{ id: 'claude-haiku-4-5', label: 'Haiku 4.5', desc: 'Fastest' },
];
@@ -437,6 +441,8 @@ export function resolveThinking(
spec?: string,
): { type: 'adaptive' } | { type: 'enabled'; budgetTokens: number } | undefined {
const { model, effort } = parseModelSpec(spec);
+ // Fable: thinking is always on, omit the param to let it default (explicit 'disabled' returns 400)
+ if (model.includes('fable')) return undefined;
if (model.includes('opus') && effort === 'max') return { type: 'enabled', budgetTokens: 128_000 };
if (!model || model.includes('opus')) return { type: 'adaptive' };
if (model.includes('sonnet')) return { type: 'enabled', budgetTokens: 10_000 };
@@ -746,7 +752,7 @@ export async function startChat(
telosTaskId?: string;
agentName?: string;
},
-) {
+): Promise {
return withSpanAsync(
'chat.start',
{
@@ -1082,12 +1088,37 @@ async function _startChatInner(
);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Unknown error';
+ const isModelError =
+ message.includes('not found') ||
+ message.includes('404') ||
+ message.includes('not have access') ||
+ message.includes('disallowed');
+ const requestedModel = parseModelSpec(options.model).model;
+ const canFallback = isModelError && requestedModel && requestedModel !== FALLBACK_MODEL;
+
if (message.includes('No conversation found') && options.resume) {
log.warn('SDK rejected resume, session expired', { sessionId: options.resume, cwd });
send(transport, {
type: 'error',
error: 'Session expired. Send your message again to start fresh.',
});
+ } else if (canFallback) {
+ log.warn('Model unavailable, falling back', {
+ requested: requestedModel,
+ fallback: FALLBACK_MODEL,
+ error: message,
+ });
+ send(transport, {
+ type: 'system',
+ text: `Model "${requestedModel}" unavailable, falling back to ${FALLBACK_MODEL}.`,
+ });
+ const failedSession = registry.get(clientId);
+ if (failedSession) cleanupSessionWorktrees(failedSession);
+ registry.abort(clientId);
+ return startChat(transport, clientId, prompt, {
+ ...options,
+ model: FALLBACK_MODEL,
+ });
} else {
log.error('startChat failed after register, cleaning up', { clientId, error: message });
send(transport, { type: 'error', error: message });