jeo-code 0.1.0 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.ja.md +160 -0
  2. package/README.ko.md +160 -0
  3. package/README.md +115 -297
  4. package/README.zh.md +160 -0
  5. package/package.json +11 -6
  6. package/scripts/install.sh +28 -28
  7. package/scripts/uninstall.sh +17 -15
  8. package/src/AGENTS.md +50 -0
  9. package/src/agent/AGENTS.md +49 -0
  10. package/src/agent/bash-fixups.ts +103 -0
  11. package/src/agent/compaction.ts +410 -19
  12. package/src/agent/config-schema.ts +119 -5
  13. package/src/agent/context-files.ts +314 -17
  14. package/src/agent/dev/AGENTS.md +36 -0
  15. package/src/agent/dev/advanced-analyzer.ts +12 -0
  16. package/src/agent/dev/evolution-bridge.ts +82 -0
  17. package/src/agent/dev/evolution-logger.ts +41 -0
  18. package/src/agent/dev/self-analysis.ts +64 -0
  19. package/src/agent/dev/self-improve.ts +24 -0
  20. package/src/agent/dev/spec-automation.ts +49 -0
  21. package/src/agent/engine.ts +808 -54
  22. package/src/agent/hooks.ts +273 -0
  23. package/src/agent/loop.ts +21 -1
  24. package/src/agent/memory.ts +201 -0
  25. package/src/agent/model-recency.ts +32 -0
  26. package/src/agent/output-minimizer.ts +108 -0
  27. package/src/agent/output-util.ts +64 -0
  28. package/src/agent/plan.ts +187 -0
  29. package/src/agent/seed.ts +52 -0
  30. package/src/agent/session.ts +235 -21
  31. package/src/agent/state.ts +286 -39
  32. package/src/agent/step-budget.ts +232 -0
  33. package/src/agent/subagents.ts +223 -26
  34. package/src/agent/task-tool.ts +272 -0
  35. package/src/agent/todo-tool.ts +87 -0
  36. package/src/agent/tokenizer.ts +117 -0
  37. package/src/agent/tool-registry.ts +54 -0
  38. package/src/agent/tools.ts +624 -103
  39. package/src/agent/web-search.ts +538 -0
  40. package/src/ai/AGENTS.md +44 -0
  41. package/src/ai/index.ts +1 -0
  42. package/src/ai/model-catalog-compat.ts +3 -1
  43. package/src/ai/model-catalog.ts +74 -9
  44. package/src/ai/model-discovery.ts +215 -17
  45. package/src/ai/model-manager.ts +346 -32
  46. package/src/ai/model-picker.ts +1 -1
  47. package/src/ai/model-registry.ts +4 -2
  48. package/src/ai/pricing.ts +84 -0
  49. package/src/ai/provider-registry.ts +23 -0
  50. package/src/ai/provider-status.ts +60 -16
  51. package/src/ai/providers/AGENTS.md +42 -0
  52. package/src/ai/providers/anthropic.ts +250 -31
  53. package/src/ai/providers/antigravity.ts +219 -0
  54. package/src/ai/providers/errors.ts +15 -1
  55. package/src/ai/providers/gemini.ts +196 -13
  56. package/src/ai/providers/ollama.ts +37 -7
  57. package/src/ai/providers/openai-responses.ts +173 -0
  58. package/src/ai/providers/openai.ts +64 -12
  59. package/src/ai/sse.ts +4 -1
  60. package/src/ai/types.ts +18 -1
  61. package/src/auth/AGENTS.md +41 -0
  62. package/src/auth/callback-server.ts +6 -1
  63. package/src/auth/flows/AGENTS.md +32 -0
  64. package/src/auth/flows/antigravity.ts +151 -0
  65. package/src/auth/flows/google-project.ts +190 -0
  66. package/src/auth/flows/google.ts +39 -18
  67. package/src/auth/flows/index.ts +15 -5
  68. package/src/auth/flows/openai.ts +2 -2
  69. package/src/auth/oauth.ts +8 -0
  70. package/src/auth/refresh.ts +44 -27
  71. package/src/auth/storage.ts +149 -26
  72. package/src/auth/types.ts +1 -1
  73. package/src/autopilot.ts +362 -0
  74. package/src/bun-imports.d.ts +4 -0
  75. package/src/cli/AGENTS.md +39 -0
  76. package/src/cli/runner.ts +148 -14
  77. package/src/cli.ts +13 -4
  78. package/src/commands/AGENTS.md +40 -0
  79. package/src/commands/approve.ts +62 -3
  80. package/src/commands/auth.ts +167 -25
  81. package/src/commands/chat.ts +37 -8
  82. package/src/commands/deep-interview.ts +633 -175
  83. package/src/commands/doctor.ts +84 -37
  84. package/src/commands/evolve-core.ts +18 -0
  85. package/src/commands/evolve.ts +2 -1
  86. package/src/commands/export.ts +176 -0
  87. package/src/commands/gjc.ts +52 -0
  88. package/src/commands/launch.ts +3549 -240
  89. package/src/commands/mcp.ts +3 -3
  90. package/src/commands/ooo-seed.ts +19 -0
  91. package/src/commands/ralplan.ts +253 -35
  92. package/src/commands/resume.ts +1 -1
  93. package/src/commands/session.ts +183 -0
  94. package/src/commands/setup-helpers.ts +10 -3
  95. package/src/commands/setup.ts +57 -16
  96. package/src/commands/skills.ts +78 -18
  97. package/src/commands/state.ts +198 -0
  98. package/src/commands/status.ts +84 -0
  99. package/src/commands/team.ts +340 -212
  100. package/src/commands/ultragoal.ts +122 -61
  101. package/src/commands/update.ts +244 -0
  102. package/src/ledger.ts +270 -0
  103. package/src/mcp/AGENTS.md +38 -0
  104. package/src/mcp/server.ts +115 -14
  105. package/src/mcp/tools.ts +42 -22
  106. package/src/md-modules.d.ts +4 -0
  107. package/src/prompts/AGENTS.md +41 -0
  108. package/src/prompts/agents/AGENTS.md +35 -0
  109. package/src/prompts/agents/architect.md +35 -0
  110. package/src/prompts/agents/critic.md +37 -0
  111. package/src/prompts/agents/executor.md +36 -0
  112. package/src/prompts/agents/planner.md +37 -0
  113. package/src/prompts/skills/AGENTS.md +36 -0
  114. package/src/prompts/skills/deep-dive/AGENTS.md +31 -0
  115. package/src/prompts/skills/deep-dive/SKILL.md +13 -0
  116. package/src/prompts/skills/deep-interview/AGENTS.md +31 -0
  117. package/src/prompts/skills/deep-interview/SKILL.md +12 -0
  118. package/src/prompts/skills/gjc/AGENTS.md +31 -0
  119. package/src/prompts/skills/gjc/SKILL.md +15 -0
  120. package/src/prompts/skills/ralplan/AGENTS.md +31 -0
  121. package/src/prompts/skills/ralplan/SKILL.md +11 -0
  122. package/src/prompts/skills/team/AGENTS.md +31 -0
  123. package/src/prompts/skills/team/SKILL.md +11 -0
  124. package/src/prompts/skills/ultragoal/AGENTS.md +31 -0
  125. package/src/prompts/skills/ultragoal/SKILL.md +11 -0
  126. package/src/skills/AGENTS.md +38 -0
  127. package/src/skills/catalog.ts +565 -31
  128. package/src/tui/AGENTS.md +43 -0
  129. package/src/tui/app.ts +1181 -92
  130. package/src/tui/components/AGENTS.md +42 -0
  131. package/src/tui/components/ascii-art.ts +257 -15
  132. package/src/tui/components/autocomplete.ts +98 -16
  133. package/src/tui/components/autopilot-status.ts +65 -0
  134. package/src/tui/components/category-index.ts +49 -0
  135. package/src/tui/components/code-view.ts +54 -11
  136. package/src/tui/components/color.ts +171 -2
  137. package/src/tui/components/config-panel.ts +82 -15
  138. package/src/tui/components/duration.ts +38 -0
  139. package/src/tui/components/evolution.ts +3 -3
  140. package/src/tui/components/footer.ts +91 -42
  141. package/src/tui/components/forge.ts +426 -31
  142. package/src/tui/components/hints.ts +54 -0
  143. package/src/tui/components/hud.ts +73 -0
  144. package/src/tui/components/index.ts +4 -0
  145. package/src/tui/components/input-box.ts +150 -0
  146. package/src/tui/components/layout.ts +11 -3
  147. package/src/tui/components/live-model-picker.ts +108 -0
  148. package/src/tui/components/markdown-table.ts +140 -0
  149. package/src/tui/components/markdown-text.ts +97 -0
  150. package/src/tui/components/meter.ts +4 -1
  151. package/src/tui/components/model-picker.ts +3 -2
  152. package/src/tui/components/provider-picker.ts +3 -2
  153. package/src/tui/components/section.ts +70 -0
  154. package/src/tui/components/select-list.ts +40 -10
  155. package/src/tui/components/skill-picker.ts +25 -0
  156. package/src/tui/components/slash.ts +244 -21
  157. package/src/tui/components/status.ts +272 -11
  158. package/src/tui/components/step-timeline.ts +218 -0
  159. package/src/tui/components/stream.ts +26 -9
  160. package/src/tui/components/themes.ts +212 -6
  161. package/src/tui/components/todo-card.ts +47 -0
  162. package/src/tui/components/tool-list.ts +58 -12
  163. package/src/tui/components/transcript.ts +120 -0
  164. package/src/tui/components/update-box.ts +31 -0
  165. package/src/tui/components/welcome.ts +162 -0
  166. package/src/tui/components/width.ts +163 -0
  167. package/src/tui/monitoring/AGENTS.md +31 -0
  168. package/src/tui/monitoring/hud-view.ts +55 -0
  169. package/src/tui/renderer.ts +112 -3
  170. package/src/tui/terminal.ts +40 -33
  171. package/src/util/AGENTS.md +39 -0
  172. package/src/util/clipboard-image.ts +118 -0
  173. package/src/util/env.ts +12 -0
  174. package/src/util/provider-error.ts +78 -0
  175. package/src/util/retry.ts +91 -6
  176. package/src/util/update-check.ts +64 -0
  177. package/src/commands/models.ts +0 -104
@@ -1,24 +1,25 @@
1
1
  /**
2
2
  * Provider credential/status inventory — the shared source of truth behind
3
- * `joc models`, the TUI `/provider` command, and `joc doctor`. Reports, for each
4
- * provider, how it will authenticate (API key / OAuth / keyless / none), its
5
- * effective base URL, and whether it is ready to serve a request.
3
+ * the TUI `/provider` command, `jeo doctor`, and setup probes. Reports, for
4
+ * each provider, how it will authenticate (API key / OAuth / keyless / none),
5
+ * its effective base URL, and whether it is ready to serve a request.
6
6
  */
7
- import { readGlobalConfig, type Config } from "../agent/state";
8
- import { resolveCredential, type AuthProvider } from "../auth";
7
+ import { readGlobalConfig, type Config, type StoredOAuth } from "../agent/state";
8
+ import type { AuthProvider, Credential } from "../auth";
9
+ import { OAUTH_FLOW_REGISTRY } from "../auth/flows";
9
10
  import type { ProviderName } from "./types";
10
11
 
11
- export const PROVIDER_NAMES: readonly ProviderName[] = ["anthropic", "openai", "gemini", "ollama"];
12
+ export const PROVIDER_NAMES: readonly ProviderName[] = ["anthropic", "openai", "gemini", "antigravity", "ollama"];
12
13
 
13
14
  /** Cloud providers that authenticate via API key / OAuth. Ollama is keyless. */
14
- export const CLOUD_PROVIDERS: readonly AuthProvider[] = ["anthropic", "openai", "gemini"];
15
+ export const CLOUD_PROVIDERS: readonly AuthProvider[] = ["anthropic", "openai", "gemini", "antigravity"];
15
16
 
16
17
  export type CredentialKind = "api_key" | "oauth" | "keyless" | "none";
17
18
 
18
19
  export interface ProviderStatus {
19
20
  name: ProviderName;
20
21
  kind: CredentialKind;
21
- /** Display label, e.g. "API key", "OAuth", "keyless (local)", "none (run 'joc setup')". */
22
+ /** Display label, e.g. "API key", "OAuth", "keyless (local)", "none (run 'jeo setup')". */
22
23
  label: string;
23
24
  /** Effective base URL when relevant (ollama / openai-compatible). */
24
25
  baseUrl?: string;
@@ -30,7 +31,7 @@ export interface ProviderStatus {
30
31
 
31
32
  /** The uppercase `<PROVIDER>_API_KEY` env var name for a cloud provider. */
32
33
  export function providerEnvVar(name: ProviderName): string | undefined {
33
- if (name === "ollama") return undefined;
34
+ if (name === "ollama" || name === "antigravity") return undefined;
34
35
  return `${name.toUpperCase()}_API_KEY`;
35
36
  }
36
37
 
@@ -44,10 +45,32 @@ export function credentialLabel(kind: CredentialKind): string {
44
45
  case "keyless":
45
46
  return "keyless (local)";
46
47
  case "none":
47
- return "none (run 'joc setup' or 'joc auth login')";
48
+ return "none (run 'jeo setup' or 'jeo auth login')";
48
49
  }
49
50
  }
50
51
 
52
+ function oauthAccess(stored: string | StoredOAuth | undefined): string | undefined {
53
+ if (!stored) return undefined;
54
+ return typeof stored === "string" ? stored : stored.access;
55
+ }
56
+
57
+ function configuredCredential(provider: AuthProvider, cfg: Config): Credential {
58
+ const stored = cfg.oauth?.[provider];
59
+ const oauth = oauthAccess(stored);
60
+ if (oauth) return { kind: "oauth", provider, token: oauth, projectId: typeof stored === "object" ? stored.projectId : undefined };
61
+ const key = cfg.providers?.[provider];
62
+ if (key) return { kind: "api_key", provider, token: key };
63
+ return { kind: "none", provider };
64
+ }
65
+
66
+ /** Match the real call path: API keys are broader and win whenever both key + OAuth exist. */
67
+ function effectiveCredential(provider: AuthProvider, cred: Credential, cfg: Config): Credential {
68
+ const key = cfg.providers?.[provider];
69
+ if (cred.kind === "oauth" && key) return { kind: "api_key", provider, token: key };
70
+ return cred;
71
+ }
72
+
73
+
51
74
  /** Resolve the status of a single provider. */
52
75
  export async function describeProvider(name: ProviderName, config?: Config): Promise<ProviderStatus> {
53
76
  const cfg = config ?? (await readGlobalConfig());
@@ -55,15 +78,36 @@ export async function describeProvider(name: ProviderName, config?: Config): Pro
55
78
  const baseUrl = cfg.ollamaBaseUrl ?? "http://localhost:11434";
56
79
  return { name, kind: "keyless", label: credentialLabel("keyless"), baseUrl, ready: true };
57
80
  }
58
- const cred = await resolveCredential(name as AuthProvider);
59
- const kind: CredentialKind = cred.kind === "api_key" ? "api_key" : cred.kind === "oauth" ? "oauth" : "none";
60
- const baseUrl = name === "openai" ? cfg.openaiBaseUrl : undefined;
61
- // An OpenAI-compatible local server (LM Studio, vLLM) needs no key.
62
- const ready = kind !== "none" || (name === "openai" && !!baseUrl);
81
+ const ownProvider = name as AuthProvider;
82
+ const ownCred = configuredCredential(ownProvider, cfg);
83
+ // Antigravity prefers its own login but accepts a gemini-cli OAuth fallback.
84
+ const cred = name === "antigravity" && ownCred.kind === "none" ? configuredCredential("gemini", cfg) : ownCred;
85
+ const credentialProvider: AuthProvider = name === "antigravity" && ownCred.kind === "none" ? "gemini" : ownProvider;
86
+ const effective = name === "antigravity" ? cred : effectiveCredential(credentialProvider, cred, cfg);
87
+ const kind: CredentialKind = effective.kind === "api_key" ? "api_key" : effective.kind === "oauth" ? "oauth" : "none";
88
+ const baseUrl = name === "openai" && kind !== "oauth" ? cfg.openaiBaseUrl : undefined;
89
+ let ready = kind !== "none" || (name === "openai" && !!cfg.openaiBaseUrl);
90
+ let label = ready && kind === "none" ? "keyless (local base URL)" : credentialLabel(kind);
91
+ if (name === "antigravity") {
92
+ const hasOwnOAuth = ownCred.kind === "oauth";
93
+ const hasGeminiFallback = !hasOwnOAuth && configuredCredential("gemini", cfg).kind === "oauth";
94
+ ready = hasOwnOAuth;
95
+ label = hasOwnOAuth
96
+ ? "OAuth (Antigravity Cloud Code Assist)"
97
+ : hasGeminiFallback
98
+ ? "OAuth catalog via Gemini CLI; calls need 'jeo auth login antigravity'"
99
+ : "none (run 'jeo auth login antigravity')";
100
+ } else if (kind === "oauth" && OAUTH_FLOW_REGISTRY[credentialProvider]?.verifiedEndToEnd === false) {
101
+ ready = false;
102
+ label = "OAuth (API key needed)";
103
+ } else if (name === "gemini" && kind === "oauth") {
104
+ // gemini-cli OAuth is served end-to-end via Cloud Code Assist — no API key.
105
+ label = "OAuth (Gemini CLI / Cloud Code Assist)";
106
+ }
63
107
  return {
64
108
  name,
65
109
  kind,
66
- label: ready && kind === "none" ? "keyless (local base URL)" : credentialLabel(kind),
110
+ label,
67
111
  baseUrl,
68
112
  envVar: providerEnvVar(name),
69
113
  ready,
@@ -0,0 +1,42 @@
1
+ <!-- Parent: ../../AGENTS.md -->
2
+ <!-- Generated: 2026-06-11 | Updated: 2026-06-11 -->
3
+
4
+ # providers
5
+
6
+ ## Purpose
7
+ Concrete implementations for various LLM providers, translating generic requests into provider-specific API calls.
8
+
9
+ ## Key Files
10
+ | File | Description |
11
+ |------|-------------|
12
+ | `anthropic.ts` | Anthropic Claude integration |
13
+ | `openai.ts` | OpenAI (and Codex backend) integration |
14
+ | `gemini.ts` | Google Gemini (and Cloud Code Assist) integration |
15
+ | `antigravity.ts` | Antigravity desktop-app OAuth client integration |
16
+ | `ollama.ts` | Local Ollama integration |
17
+
18
+ ## Subdirectories
19
+ *(None)*
20
+
21
+ ## For AI Agents
22
+
23
+ ### Working In This Directory
24
+ - Each provider must handle its specific tool-calling syntax and streaming chunk format.
25
+ - Ensure strict parsing of SSE (Server-Sent Events) streams.
26
+
27
+ ### Testing Requirements
28
+ - Unit test stream parsing with mock payloads.
29
+
30
+ ### Common Patterns
31
+ - Native fetch calls with `for await` loops over text decoding streams.
32
+
33
+ ## Dependencies
34
+
35
+ ### Internal
36
+ - `src/ai/types.ts` for interfaces.
37
+ - `src/auth/` for retrieving tokens.
38
+
39
+ ### External
40
+ - HTTP `fetch`.
41
+
42
+ <!-- MANUAL: -->
@@ -1,54 +1,216 @@
1
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
1
2
  import type { Credential } from "../../auth";
2
3
  import type { CallOptions, Message, ProviderAdapter } from "../types";
3
4
  import { readSse } from "../sse";
4
- import { providerHttpError } from "./errors";
5
+ import { ProviderHttpError, parseRetryAfter, parseRetryFromBody, providerHttpError } from "./errors";
5
6
 
6
7
  const ANTHROPIC_URL = "https://api.anthropic.com/v1/messages";
7
8
 
8
- function anthropicPayload(messages: Message[], options: CallOptions, stream: boolean): string {
9
- const resolvedModel = options.model.startsWith("anthropic/") ? options.model.slice(10) : options.model;
10
- const model = resolvedModel.includes("sonnet") ? "claude-3-5-sonnet-20241022" : resolvedModel;
9
+ const DEPRECATED_TEMPERATURE = "`temperature` is deprecated for this model.";
10
+ const CLAUDE_CODE_VERSION = "2.1.63";
11
+ const CLAUDE_CODE_SYSTEM_INSTRUCTION = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
12
+ const CLAUDE_BILLING_HEADER_PREFIX = "x-anthropic-billing-header:";
13
+ const ANTHROPIC_OAUTH_BETA = [
14
+ "claude-code-20250219",
15
+ "oauth-2025-04-20",
16
+ "interleaved-thinking-2025-05-14",
17
+ "context-management-2025-06-27",
18
+ "prompt-caching-scope-2026-01-05",
19
+ ].join(",");
20
+
21
+ interface AnthropicSystemBlock {
22
+ type: "text";
23
+ text: string;
24
+ cache_control?: { type: "ephemeral" };
25
+ }
26
+
27
+ function stripAnthropicPrefix(model: string): string {
28
+ return model.startsWith("anthropic/") ? model.slice(10) : model;
29
+ }
30
+
31
+ function shouldUseClaudeCodeOAuthShape(model: string, credential: Credential): boolean {
32
+ return credential.kind === "oauth" && !model.startsWith("claude-3-5-haiku");
33
+ }
34
+
35
+ function createClaudeCloakingUserId(): string {
36
+ return `user_${randomBytes(32).toString("hex")}_account_${randomUUID().toLowerCase()}_session_${randomUUID().toLowerCase()}`;
37
+ }
38
+
39
+ function createClaudeBillingHeader(payload: unknown): string {
40
+ const payloadJson = JSON.stringify(payload) ?? "";
41
+ const cch = createHash("sha256").update(payloadJson).digest("hex").slice(0, 5);
42
+ const randomBytes = new Uint8Array(2);
43
+ crypto.getRandomValues(randomBytes);
44
+ const buildHash = Array.from(randomBytes, byte => byte.toString(16).padStart(2, "0")).join("").slice(0, 3);
45
+ return `${CLAUDE_BILLING_HEADER_PREFIX} cc_version=${CLAUDE_CODE_VERSION}.${buildHash}; cc_entrypoint=cli; cch=${cch};`;
46
+ }
47
+
48
+ function anthropicSystemBlocks(
49
+ systemPrompt: string | undefined,
50
+ model: string,
51
+ credential: Credential,
52
+ billingPayload: Record<string, unknown>,
53
+ ): AnthropicSystemBlock[] | undefined {
54
+ const blocks: AnthropicSystemBlock[] = [];
55
+ if (shouldUseClaudeCodeOAuthShape(model, credential)) {
56
+ const billingSeed = systemPrompt ? { ...billingPayload, system: [systemPrompt] } : billingPayload;
57
+ blocks.push(
58
+ { type: "text", text: createClaudeBillingHeader(billingSeed) },
59
+ { type: "text", text: CLAUDE_CODE_SYSTEM_INSTRUCTION },
60
+ );
61
+ }
62
+ if (systemPrompt) {
63
+ blocks.push({ type: "text", text: systemPrompt });
64
+ }
65
+ if (blocks.length === 0) return undefined;
66
+
67
+ // Prompt caching (gjc parity): Anthropic cache breakpoints are cumulative. Put a
68
+ // single breakpoint on the last system block so Claude Code OAuth prelude + the
69
+ // real system prompt are cached together without burning multiple slots.
70
+ blocks[blocks.length - 1] = { ...blocks[blocks.length - 1], cache_control: { type: "ephemeral" } };
71
+ return blocks;
72
+ }
73
+
74
+ export function anthropicPayload(
75
+ messages: Message[],
76
+ options: CallOptions,
77
+ stream: boolean,
78
+ includeTemperature: boolean,
79
+ credential: Credential = { kind: "none", provider: "anthropic" },
80
+ ): string {
81
+ const model = stripAnthropicPrefix(options.model);
11
82
  const systemPrompt = options.systemPrompt ?? messages.find(m => m.role === "system")?.content;
12
- const anthropicMessages = messages.filter(m => m.role !== "system").map(m => ({ role: m.role, content: m.content }));
83
+ // Image attachments (clipboard paste) become Anthropic content blocks; plain
84
+ // string content is kept for text-only messages (the overwhelmingly common case).
85
+ type ContentBlock = Record<string, unknown>;
86
+ const anthropicMessages: { role: string; content: string | ContentBlock[] }[] =
87
+ messages.filter(m => m.role !== "system").map(m => ({
88
+ role: m.role,
89
+ content: m.images?.length
90
+ ? [
91
+ ...m.images.map((img): ContentBlock => ({ type: "image", source: { type: "base64", media_type: img.mediaType, data: img.data } })),
92
+ ...(m.content ? [{ type: "text", text: m.content } as ContentBlock] : []),
93
+ ]
94
+ : m.content,
95
+ }));
96
+ // Conversation prompt caching (gjc parity — the main same-model latency gap):
97
+ // one breakpoint on the LAST message caches the entire conversation prefix, so
98
+ // each agent-loop step only pays input processing for the new tail instead of
99
+ // re-ingesting the whole growing history. Combined with the system-block
100
+ // breakpoint this uses 2 of Anthropic's 4 slots. Sub-minimum prompts (<1024
101
+ // tokens) ignore the marker harmlessly.
102
+ const last = anthropicMessages[anthropicMessages.length - 1];
103
+ if (last) {
104
+ if (typeof last.content === "string") {
105
+ if (last.content) last.content = [{ type: "text", text: last.content, cache_control: { type: "ephemeral" } }];
106
+ } else if (last.content.length > 0) {
107
+ const tail = last.content[last.content.length - 1]!;
108
+ last.content[last.content.length - 1] = { ...tail, cache_control: { type: "ephemeral" } };
109
+ }
110
+ }
13
111
  const payload: Record<string, unknown> = {
14
112
  model,
15
113
  messages: anthropicMessages,
16
114
  max_tokens: options.maxTokens ?? 4000,
17
- temperature: options.temperature ?? 0.2,
18
115
  };
19
- if (systemPrompt) payload.system = systemPrompt;
116
+ if (credential.kind === "oauth") payload.metadata = { user_id: createClaudeCloakingUserId() };
117
+ if (includeTemperature && options.temperature !== undefined) payload.temperature = options.temperature;
20
118
  if (stream) payload.stream = true;
119
+ const system = anthropicSystemBlocks(systemPrompt, model, credential, payload);
120
+ if (system) payload.system = system;
21
121
  return JSON.stringify(payload);
22
122
  }
23
123
 
124
+ export function anthropicRequest(
125
+ messages: Message[],
126
+ options: CallOptions,
127
+ credential: Credential,
128
+ stream: boolean,
129
+ includeTemperature: boolean,
130
+ ): { url: string; headers: Record<string, string>; body: string } {
131
+ return {
132
+ url: ANTHROPIC_URL,
133
+ headers: headersFor(credential, stream),
134
+ body: anthropicPayload(messages, options, stream, includeTemperature, credential),
135
+ };
136
+ }
137
+
138
+ function isDeprecatedTemperatureError(status: number, detail: string): boolean {
139
+ return status === 400 && detail.includes(DEPRECATED_TEMPERATURE);
140
+ }
141
+
142
+ async function postAnthropic(
143
+ messages: Message[],
144
+ options: CallOptions,
145
+ credential: Credential,
146
+ stream: boolean,
147
+ ): Promise<Response> {
148
+ const send = (includeTemperature: boolean) => {
149
+ const { url, headers, body } = anthropicRequest(messages, options, credential, stream, includeTemperature);
150
+ return fetch(url, { method: "POST", headers, body, signal: options.signal });
151
+ };
152
+
153
+ let response = await send(true);
154
+ if (response.ok) return response;
155
+
156
+ const detail = await response.text().catch(() => "");
157
+ if (isDeprecatedTemperatureError(response.status, detail)) {
158
+ response = await send(false);
159
+ if (response.ok) return response;
160
+ throw await providerHttpError("Anthropic", response, stream ? "(stream)" : undefined);
161
+ }
162
+
163
+ throw new ProviderHttpError(
164
+ "Anthropic",
165
+ response.status,
166
+ detail,
167
+ stream ? "(stream)" : undefined,
168
+ parseRetryAfter(response.headers.get("retry-after")) ?? parseRetryFromBody(detail),
169
+ );
170
+ }
171
+
172
+ /** Anthropic usage: with prompt caching the input splits into uncached + cache read +
173
+ * cache creation. Sum them so reported input reflects the TRUE prompt size. */
174
+ interface AnthropicUsage {
175
+ input_tokens?: number;
176
+ output_tokens?: number;
177
+ cache_read_input_tokens?: number;
178
+ cache_creation_input_tokens?: number;
179
+ }
180
+ export function totalInputTokens(u: AnthropicUsage): number {
181
+ return (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
182
+ }
183
+
184
+ /** Round-5 #1: HTTP-200-with-no-text must surface its CAUSE (stop_reason) instead
185
+ * of returning "" — an empty reply just bounces in the JSON loop, burning billed
186
+ * calls until the step budget dies. Mirrors gemini's blockedReason contract. */
187
+ function emptyCompletionError(stopReason: string | undefined): Error {
188
+ const hint = stopReason === "max_tokens"
189
+ ? " — output budget exhausted before any text; raise maxTokens or lower the thinking level"
190
+ : "";
191
+ return new Error(`Anthropic returned no content${stopReason ? ` (stop_reason=${stopReason})` : ""}${hint}.`);
192
+ }
24
193
  export const anthropicAdapter: ProviderAdapter = {
25
194
  name: "anthropic",
26
195
  async call(messages, options, credential) {
27
- const response = await fetch(ANTHROPIC_URL, {
28
- method: "POST",
29
- headers: headersFor(credential),
30
- body: anthropicPayload(messages, options, false),
31
- signal: options.signal,
32
- });
33
- if (!response.ok) throw await providerHttpError("Anthropic", response);
34
- const result = (await response.json()) as { content: { type: string; text: string }[]; usage?: { input_tokens?: number; output_tokens?: number } };
35
- if (result.usage) options.onUsage?.({ inputTokens: result.usage.input_tokens, outputTokens: result.usage.output_tokens });
36
- return result.content.find(c => c.type === "text")?.text ?? "";
196
+ const response = await postAnthropic(messages, options, credential, false);
197
+ const result = (await response.json()) as { content: { type: string; text: string }[]; stop_reason?: string; usage?: AnthropicUsage };
198
+ if (result.usage) options.onUsage?.({ inputTokens: totalInputTokens(result.usage), outputTokens: result.usage.output_tokens });
199
+ const text = result.content.find(c => c.type === "text")?.text ?? "";
200
+ if (!text) throw emptyCompletionError(result.stop_reason);
201
+ return text;
37
202
  },
38
203
  async *stream(messages, options, credential) {
39
- const response = await fetch(ANTHROPIC_URL, {
40
- method: "POST",
41
- headers: headersFor(credential),
42
- body: anthropicPayload(messages, options, true),
43
- signal: options.signal,
44
- });
45
- if (!response.ok) throw await providerHttpError("Anthropic", response, "(stream)");
204
+ const response = await postAnthropic(messages, options, credential, true);
46
205
  if (!response.body) return;
206
+ let cachedInput: number | undefined;
207
+ let yieldedAny = false;
208
+ let stopReason: string | undefined;
47
209
  for await (const data of readSse(response.body)) {
48
210
  let evt: {
49
211
  type?: string;
50
- delta?: { type?: string; text?: string };
51
- message?: { usage?: { input_tokens?: number; output_tokens?: number } };
212
+ delta?: { type?: string; text?: string; stop_reason?: string };
213
+ message?: { usage?: AnthropicUsage };
52
214
  usage?: { output_tokens?: number };
53
215
  };
54
216
  try {
@@ -57,27 +219,84 @@ export const anthropicAdapter: ProviderAdapter = {
57
219
  continue;
58
220
  }
59
221
  if (evt.type === "content_block_delta" && evt.delta?.type === "text_delta" && evt.delta.text) {
222
+ yieldedAny = true;
60
223
  yield evt.delta.text;
61
224
  } else if (evt.type === "message_start" && evt.message?.usage) {
62
- options.onUsage?.({ inputTokens: evt.message.usage.input_tokens, outputTokens: evt.message.usage.output_tokens });
63
- } else if (evt.type === "message_delta" && evt.usage) {
64
- options.onUsage?.({ outputTokens: evt.usage.output_tokens });
225
+ // Cache only usage is reported ONCE at message_delta so an accumulating
226
+ // sink can't double-count input (and a pre-first-chunk retry that replays
227
+ // message_start is harmless).
228
+ cachedInput = totalInputTokens(evt.message.usage);
229
+ } else if (evt.type === "message_delta") {
230
+ if (evt.delta?.stop_reason) stopReason = evt.delta.stop_reason;
231
+ if (evt.usage) options.onUsage?.({ inputTokens: cachedInput, outputTokens: evt.usage.output_tokens });
65
232
  }
66
233
  }
234
+ if (!yieldedAny) throw emptyCompletionError(stopReason);
67
235
  },
68
236
  };
237
+ function mapStainlessOs(platform: string): "MacOS" | "Windows" | "Linux" | "FreeBSD" | `Other::${string}` {
238
+ switch (platform.toLowerCase()) {
239
+ case "darwin":
240
+ return "MacOS";
241
+ case "windows":
242
+ case "win32":
243
+ return "Windows";
244
+ case "linux":
245
+ return "Linux";
246
+ case "freebsd":
247
+ return "FreeBSD";
248
+ default:
249
+ return `Other::${platform.toLowerCase()}`;
250
+ }
251
+ }
252
+
253
+ function mapStainlessArch(arch: string): "x64" | "arm64" | "x86" | `other::${string}` {
254
+ switch (arch.toLowerCase()) {
255
+ case "amd64":
256
+ case "x64":
257
+ return "x64";
258
+ case "arm64":
259
+ case "aarch64":
260
+ return "arm64";
261
+ case "386":
262
+ case "x86":
263
+ case "ia32":
264
+ return "x86";
265
+ default:
266
+ return `other::${arch.toLowerCase()}`;
267
+ }
268
+ }
269
+
270
+ function claudeCodeOAuthHeaders(stream: boolean): Record<string, string> {
271
+ return {
272
+ accept: stream ? "text/event-stream" : "application/json",
273
+ "anthropic-beta": ANTHROPIC_OAUTH_BETA,
274
+ "anthropic-dangerous-direct-browser-access": "true",
275
+ "user-agent": `claude-cli/${CLAUDE_CODE_VERSION} (external, cli)`,
276
+ "x-app": "cli",
277
+ "x-stainless-arch": mapStainlessArch(process.arch),
278
+ "x-stainless-lang": "js",
279
+ "x-stainless-os": mapStainlessOs(process.platform),
280
+ "x-stainless-package-version": "0.74.0",
281
+ "x-stainless-retry-count": "0",
282
+ "x-stainless-runtime": "node",
283
+ "x-stainless-runtime-version": "v24.3.0",
284
+ "x-stainless-timeout": "600",
285
+ };
286
+ }
69
287
 
70
- function headersFor(credential: Credential): Record<string, string> {
288
+ function headersFor(credential: Credential, stream: boolean): Record<string, string> {
71
289
  if (credential.kind === "oauth") {
72
290
  return {
73
291
  "content-type": "application/json",
74
292
  authorization: `Bearer ${credential.token}`,
75
293
  "anthropic-version": "2023-06-01",
76
- "anthropic-beta": "oauth-2025-04-20",
294
+ ...claudeCodeOAuthHeaders(stream),
77
295
  };
78
296
  }
79
297
  if (credential.kind === "api_key") {
80
298
  return {
299
+ accept: stream ? "text/event-stream" : "application/json",
81
300
  "content-type": "application/json",
82
301
  "x-api-key": credential.token,
83
302
  "anthropic-version": "2023-06-01",