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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ The `teleton setup` wizard generates a fully configured `~/.teleton/config.yaml`
agent:
provider: "anthropic" # anthropic | claude-code | openai | google | xai | groq | openrouter | moonshot | mistral | cerebras | zai | minimax | huggingface | nvidia | gocoon | local
api_key: "sk-ant-api03-..."
model: "claude-opus-4-6"
model: "claude-opus-4-8"
utility_model: "claude-haiku-4-5-20251001" # optional: summarization, compaction, vision
max_agentic_iterations: 5
session_reset_policy:
Expand Down
4 changes: 2 additions & 2 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
agent:
provider: "anthropic" # anthropic | openai | google | xai | groq | openrouter | nvidia
api_key: "YOUR_API_KEY"
model: "claude-haiku-4-5-20251001" # Model ID (varies by provider)
# utility_model: "claude-3-5-haiku-20241022" # Optional: cheap model for summarization
model: "claude-opus-4-8" # Model ID (varies by provider)
# utility_model: "claude-haiku-4-5-20251001" # Optional: cheap model for summarization
max_tokens: 4096
temperature: 0.7
# system_prompt: null # Custom system prompt override (null = use SOUL.md)
Expand Down
8 changes: 4 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ LLM provider and agentic loop configuration.
|-----|------|---------|-------------|
| `agent.provider` | `enum` | `"anthropic"` | LLM provider. One of: `anthropic`, `claude-code`, `openai`, `google`, `xai`, `groq`, `openrouter`, `moonshot`, `mistral`, `cerebras`, `zai`, `minimax`, `huggingface`, `nvidia`, `gocoon`, `local`. |
| `agent.api_key` | `string` | `""` | API key for the chosen provider. Can be overridden with `TELETON_API_KEY` env var. |
| `agent.model` | `string` | `"claude-haiku-4-5-20251001"` | Primary model ID. Auto-detected from provider if not set (only for non-Anthropic providers). |
| `agent.model` | `string` | `"claude-opus-4-8"` | Primary model ID. Auto-detected from provider if not set (only for non-Anthropic providers). |
| `agent.utility_model` | `string` | *auto-detected* | Cheap/fast model used for summarization and compaction. If omitted, the platform selects one based on the provider (e.g., `claude-haiku-4-5-20251001` for Anthropic, `gpt-4o-mini` for OpenAI). |
| `agent.base_url` | `string` | *optional* | Base URL for local LLM server (e.g., `http://localhost:11434/v1`). Must be a valid URL. |
| `agent.max_tokens` | `number` | `4096` | Maximum tokens in each LLM response. |
Expand All @@ -65,7 +65,7 @@ Controls when conversation sessions are cleared, giving the agent a fresh memory
agent:
provider: "anthropic"
api_key: "sk-ant-..."
model: "claude-haiku-4-5-20251001"
model: "claude-opus-4-8"
utility_model: "claude-haiku-4-5-20251001"
max_tokens: 4096
temperature: 0.7
Expand All @@ -83,7 +83,7 @@ When you change the `provider` and omit `model`, the platform auto-selects:

| Provider | Default Model | Default Utility Model |
|----------|--------------|----------------------|
| `anthropic` | `claude-haiku-4-5-20251001` | `claude-haiku-4-5-20251001` |
| `anthropic` | `claude-opus-4-8` | `claude-haiku-4-5-20251001` |
| `codex` | `gpt-5.5` | `gpt-5.1-codex-mini` |
| `openai` | `gpt-5.5` | `gpt-4o-mini` |
| `google` | `gemini-2.5-flash` | `gemini-2.0-flash-lite` |
Expand Down Expand Up @@ -737,7 +737,7 @@ meta:
agent:
provider: "anthropic"
api_key: "sk-ant-..."
model: "claude-haiku-4-5-20251001"
model: "claude-opus-4-8"
max_tokens: 4096
temperature: 0.7
max_agentic_iterations: 5
Expand Down
19 changes: 19 additions & 0 deletions src/agent/__tests__/client-abort.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,23 @@ describe("chatWithContext abort signal handling", () => {
expect(completeSignal?.aborted).toBe(true);
expect(completeSignal?.reason).toBe(reason);
});

it("omits temperature for Claude Opus 4.8 requests", async () => {
const { chatWithContext } = await import("../client.js");

await chatWithContext(
{
...agentConfig(),
provider: "anthropic",
api_key: "sk-ant-test-key",
model: "claude-opus-4-8",
temperature: 0.7,
},
{
context: { messages: [], systemPrompt: "test" },
}
);

expect(getCapturedCompleteOptions()).not.toHaveProperty("temperature");
});
});
20 changes: 20 additions & 0 deletions src/agent/__tests__/runtime-retry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,26 @@ describe("AgentRuntime retry backoff", () => {
expect(chatWithContextMock).toHaveBeenCalledTimes(2);
});

it("reports Anthropic 410 responses as an unavailable model with recovery guidance", async () => {
const runtime = await createRuntime();
chatWithContextMock.mockResolvedValueOnce(errorResponse("410 status code (no body)"));

await expect(
runtime.processMessage({
chatId: "1001",
userMessage: "hello",
userName: "Owner",
toolContext: {
senderId: 1001,
config: makeConfig(),
},
})
).rejects.toThrow(
'Provider anthropic rejected model "claude-opus-4-6" with 410 status code (no body)'
);
expect(chatWithContextMock).toHaveBeenCalledTimes(1);
});

it("stops a pending rate-limit backoff when the signal is aborted", async () => {
vi.useFakeTimers();
const runtime = await createRuntime();
Expand Down
31 changes: 31 additions & 0 deletions src/agent/__tests__/runtime-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
sleepWithAbort,
isNetworkError,
isNetworkErrorMessage,
isModelUnavailableError,
getModelUnavailableDiagnostic,
getEmptyResponseDiagnostic,
getEmptyResponseRecoveryPrompt,
trimRagContext,
Expand Down Expand Up @@ -379,6 +381,35 @@ describe("isNetworkErrorMessage", () => {
});
});

describe("model unavailable diagnostics", () => {
it("detects Anthropic 410 responses as unavailable model errors", () => {
expect(isModelUnavailableError("410 status code (no body)")).toBe(true);
});

it("returns update guidance when the default model differs from the configured model", () => {
const result = getModelUnavailableDiagnostic({
provider: "anthropic",
model: "claude-opus-4-6",
defaultModel: "claude-opus-4-8",
errorMessage: "410 status code (no body)",
});

expect(result).toContain('Provider anthropic rejected model "claude-opus-4-6"');
expect(result).toContain('Update agent.model to "claude-opus-4-8"');
});

it("does not produce guidance for unrelated API errors", () => {
expect(
getModelUnavailableDiagnostic({
provider: "anthropic",
model: "claude-opus-4-8",
defaultModel: "claude-opus-4-8",
errorMessage: "500 internal server error",
})
).toBeNull();
});
});

// ─── T14b: getEmptyResponseDiagnostic ──────────────────────────────────

describe("getEmptyResponseDiagnostic", () => {
Expand Down
14 changes: 12 additions & 2 deletions src/agent/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ function normalizeProviderTemperature(provider: SupportedProvider, temperature:
return Math.min(Math.max(temperature, 0), NVIDIA_MAX_TEMPERATURE);
}

function shouldOmitProviderTemperature(provider: SupportedProvider, modelId: string): boolean {
if (provider !== "anthropic" && provider !== "claude-code") return false;

return /^claude-opus-4-(?:7|8)(?:$|-)/.test(modelId.toLowerCase());
}

export interface ChatOptions {
systemPrompt?: string;
context: Context;
Expand Down Expand Up @@ -134,13 +140,15 @@ export async function chatWithContext(
const completeOptions: Record<string, unknown> = {
apiKey: getEffectiveApiKey(provider, config.api_key),
maxTokens: options.maxTokens ?? config.max_tokens,
temperature,
sessionId: options.sessionId,
cacheRetention: provider === "nvidia" ? NVIDIA_CACHE_RETENTION : DEFAULT_CACHE_RETENTION,
signal: options.signal
? AbortSignal.any([options.signal, requestTimeoutSignal])
: requestTimeoutSignal,
};
if (!shouldOmitProviderTemperature(provider, config.model)) {
completeOptions.temperature = temperature;
}

let response = await complete(model, context, completeOptions as ProviderStreamOptions);

Expand Down Expand Up @@ -191,10 +199,12 @@ export function streamWithContext(config: AgentConfig, options: ChatOptions): St
const streamOptions: Record<string, unknown> = {
apiKey: getEffectiveApiKey(provider, config.api_key),
maxTokens: options.maxTokens ?? config.max_tokens,
temperature,
sessionId: options.sessionId,
cacheRetention: provider === "nvidia" ? NVIDIA_CACHE_RETENTION : DEFAULT_CACHE_RETENTION,
};
if (!shouldOmitProviderTemperature(provider, config.model)) {
streamOptions.temperature = temperature;
}

const eventStream = stream(model, context, streamOptions as ProviderStreamOptions);

Expand Down
38 changes: 38 additions & 0 deletions src/agent/runtime-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,44 @@ export function isNetworkErrorMessage(message: string): boolean {
);
}

export interface ModelUnavailableDiagnosticInput {
provider: string;
model: string;
errorMessage: string;
defaultModel?: string;
}

export function isModelUnavailableError(errorMessage: string): boolean {
const lower = errorMessage.toLowerCase();
return (
/\b410\b/.test(lower) ||
lower.includes("gone") ||
lower.includes("model not found") ||
lower.includes("model_not_found") ||
lower.includes("model unavailable") ||
lower.includes("model_unavailable") ||
lower.includes("not available or recognized") ||
lower.includes("does not exist") ||
(lower.includes("selected model") && lower.includes("not available"))
);
}

export function getModelUnavailableDiagnostic(
input: ModelUnavailableDiagnosticInput
): string | null {
if (!isModelUnavailableError(input.errorMessage)) return null;

const base =
`Provider ${input.provider} rejected model "${input.model}" with ${input.errorMessage}. ` +
"This usually means the configured model ID or provider endpoint is no longer available.";

if (input.defaultModel && input.defaultModel !== input.model) {
return `${base} Update agent.model to "${input.defaultModel}" or run teleton setup again.`;
}

return `${base} Choose a currently supported model for this provider and update agent.model.`;
}

export interface EmptyResponseDiagnosticInput {
provider: string;
model: string;
Expand Down
12 changes: 12 additions & 0 deletions src/agent/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ import {
isNetworkErrorMessage,
getEmptyResponseDiagnostic,
getEmptyResponseRecoveryPrompt,
getModelUnavailableDiagnostic,
trimRagContext,
LoopStallDetector,
sleepWithAbort,
Expand Down Expand Up @@ -1098,6 +1099,17 @@ export class AgentRuntime {
`Network error after ${NETWORK_ERROR_MAX_RETRIES} retries. Please check your connection and try again.`
);
} else {
const modelDiagnostic = getModelUnavailableDiagnostic({
provider,
model: this.config.agent.model,
errorMessage: errorMsg,
defaultModel: providerMeta.defaultModel,
});
if (modelDiagnostic) {
log.error(`🚨 ${modelDiagnostic}`);
throw new Error(modelDiagnostic);
}

log.error(`🚨 API error: ${errorMsg}`);
throw new Error(`API error: ${errorMsg || "Unknown error"}`);
}
Expand Down
2 changes: 1 addition & 1 deletion src/config/__tests__/loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ describe("Config Loader", () => {
const config = loadConfig(TEST_CONFIG_PATH);

// Agent defaults
expect(config.agent.model).toBe("claude-opus-4-6");
expect(config.agent.model).toBe("claude-opus-4-8");
expect(config.agent.max_tokens).toBe(4096);
expect(config.agent.temperature).toBe(0.7);
expect(config.agent.max_agentic_iterations).toBe(5);
Expand Down
11 changes: 8 additions & 3 deletions src/config/model-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,20 @@ export const GROQ_TTS_MODELS: ModelOption[] = [

export const MODEL_OPTIONS: Record<string, ModelOption[]> = {
anthropic: [
{
value: "claude-opus-4-8",
name: "Claude Opus 4.8",
description: "Current default, 1M ctx, $5/$25",
},
{
value: "claude-opus-4-7",
name: "Claude Opus 4.7",
description: "Most capable available, 1M ctx, reasoning, $5/$25",
description: "Previous gen, 1M ctx, reasoning, $5/$25",
},
{
value: "claude-opus-4-6",
name: "Claude Opus 4.6",
description: "Previous gen, 1M ctx, reasoning, $5/$25",
description: "Legacy, may be retired, 1M ctx, $5/$25",
},
{
value: "claude-opus-4-5-20251101",
Expand All @@ -119,7 +124,7 @@ export const MODEL_OPTIONS: Record<string, ModelOption[]> = {
{
value: "claude-haiku-4-5-20251001",
name: "Claude Haiku 4.5",
description: "Fast & cheap, 200K ctx, $1/$5 (default)",
description: "Fast & cheap, 200K ctx, $1/$5",
},
],
openai: [
Expand Down
4 changes: 2 additions & 2 deletions src/config/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const PROVIDER_REGISTRY: Record<SupportedProvider, ProviderMetadata> = {
keyPrefix: "sk-ant-",
keyHint: "sk-ant-api03-...",
consoleUrl: "https://console.anthropic.com/",
defaultModel: "claude-opus-4-6",
defaultModel: "claude-opus-4-8",
utilityModel: "claude-haiku-4-5-20251001",
toolLimit: null,
piAiProvider: "anthropic",
Expand All @@ -49,7 +49,7 @@ const PROVIDER_REGISTRY: Record<SupportedProvider, ProviderMetadata> = {
keyPrefix: "sk-ant-",
keyHint: "Auto-detected from Claude Code",
consoleUrl: "https://console.anthropic.com/",
defaultModel: "claude-opus-4-6",
defaultModel: "claude-opus-4-8",
utilityModel: "claude-haiku-4-5-20251001",
toolLimit: null,
piAiProvider: "anthropic",
Expand Down
2 changes: 1 addition & 1 deletion src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const AgentConfigSchema = z.object({
.url()
.optional()
.describe("Base URL for local LLM server (e.g. http://localhost:11434/v1)"),
model: z.string().default("claude-opus-4-6"),
model: z.string().default("claude-opus-4-8"),
utility_model: z
.string()
.optional()
Expand Down
36 changes: 36 additions & 0 deletions src/providers/__tests__/anthropic-provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import { getModelsForProvider } from "../../config/model-catalog.js";
import { getProviderMetadata } from "../../config/providers.js";
import { AgentConfigSchema } from "../../config/schema.js";
import { getProviderModel } from "../model-resolver.js";

describe("Anthropic provider registration", () => {
it("defaults to the current Opus model for primary calls", () => {
const meta = getProviderMetadata("anthropic");

expect(meta.defaultModel).toBe("claude-opus-4-8");
expect(meta.utilityModel).toBe("claude-haiku-4-5-20251001");
});

it("resolves Claude Opus 4.8 even before pi-ai ships it in the generated registry", () => {
const model = getProviderModel("anthropic", "claude-opus-4-8");

expect(model.id).toBe("claude-opus-4-8");
expect(model.api).toBe("anthropic-messages");
expect(model.provider).toBe("anthropic");
expect(model.contextWindow).toBe(1_000_000);
expect(model.maxTokens).toBe(128_000);
});

it("exposes Claude Opus 4.8 first in the setup model catalog", () => {
const models = getModelsForProvider("anthropic");

expect(models[0]?.value).toBe("claude-opus-4-8");
});

it("uses Claude Opus 4.8 in AgentConfigSchema defaults", () => {
const result = AgentConfigSchema.parse({});

expect(result.model).toBe("claude-opus-4-8");
});
});
2 changes: 1 addition & 1 deletion src/providers/__tests__/claude-code-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe("claude-code provider registration", () => {
expect(meta.displayName).toBe("Claude Code (Auto)");
expect(meta.piAiProvider).toBe("anthropic");
expect(meta.toolLimit).toBeNull();
expect(meta.defaultModel).toBe("claude-opus-4-6");
expect(meta.defaultModel).toBe("claude-opus-4-8");
expect(meta.utilityModel).toBe("claude-haiku-4-5-20251001");
expect(meta.keyPrefix).toBe("sk-ant-");
});
Expand Down
Loading
Loading