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.
- package/README.ja.md +160 -0
- package/README.ko.md +160 -0
- package/README.md +115 -297
- package/README.zh.md +160 -0
- package/package.json +11 -6
- package/scripts/install.sh +28 -28
- package/scripts/uninstall.sh +17 -15
- package/src/AGENTS.md +50 -0
- package/src/agent/AGENTS.md +49 -0
- package/src/agent/bash-fixups.ts +103 -0
- package/src/agent/compaction.ts +410 -19
- package/src/agent/config-schema.ts +119 -5
- package/src/agent/context-files.ts +314 -17
- package/src/agent/dev/AGENTS.md +36 -0
- package/src/agent/dev/advanced-analyzer.ts +12 -0
- package/src/agent/dev/evolution-bridge.ts +82 -0
- package/src/agent/dev/evolution-logger.ts +41 -0
- package/src/agent/dev/self-analysis.ts +64 -0
- package/src/agent/dev/self-improve.ts +24 -0
- package/src/agent/dev/spec-automation.ts +49 -0
- package/src/agent/engine.ts +808 -54
- package/src/agent/hooks.ts +273 -0
- package/src/agent/loop.ts +21 -1
- package/src/agent/memory.ts +201 -0
- package/src/agent/model-recency.ts +32 -0
- package/src/agent/output-minimizer.ts +108 -0
- package/src/agent/output-util.ts +64 -0
- package/src/agent/plan.ts +187 -0
- package/src/agent/seed.ts +52 -0
- package/src/agent/session.ts +235 -21
- package/src/agent/state.ts +286 -39
- package/src/agent/step-budget.ts +232 -0
- package/src/agent/subagents.ts +223 -26
- package/src/agent/task-tool.ts +272 -0
- package/src/agent/todo-tool.ts +87 -0
- package/src/agent/tokenizer.ts +117 -0
- package/src/agent/tool-registry.ts +54 -0
- package/src/agent/tools.ts +624 -103
- package/src/agent/web-search.ts +538 -0
- package/src/ai/AGENTS.md +44 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/model-catalog-compat.ts +3 -1
- package/src/ai/model-catalog.ts +74 -9
- package/src/ai/model-discovery.ts +215 -17
- package/src/ai/model-manager.ts +346 -32
- package/src/ai/model-picker.ts +1 -1
- package/src/ai/model-registry.ts +4 -2
- package/src/ai/pricing.ts +84 -0
- package/src/ai/provider-registry.ts +23 -0
- package/src/ai/provider-status.ts +60 -16
- package/src/ai/providers/AGENTS.md +42 -0
- package/src/ai/providers/anthropic.ts +250 -31
- package/src/ai/providers/antigravity.ts +219 -0
- package/src/ai/providers/errors.ts +15 -1
- package/src/ai/providers/gemini.ts +196 -13
- package/src/ai/providers/ollama.ts +37 -7
- package/src/ai/providers/openai-responses.ts +173 -0
- package/src/ai/providers/openai.ts +64 -12
- package/src/ai/sse.ts +4 -1
- package/src/ai/types.ts +18 -1
- package/src/auth/AGENTS.md +41 -0
- package/src/auth/callback-server.ts +6 -1
- package/src/auth/flows/AGENTS.md +32 -0
- package/src/auth/flows/antigravity.ts +151 -0
- package/src/auth/flows/google-project.ts +190 -0
- package/src/auth/flows/google.ts +39 -18
- package/src/auth/flows/index.ts +15 -5
- package/src/auth/flows/openai.ts +2 -2
- package/src/auth/oauth.ts +8 -0
- package/src/auth/refresh.ts +44 -27
- package/src/auth/storage.ts +149 -26
- package/src/auth/types.ts +1 -1
- package/src/autopilot.ts +362 -0
- package/src/bun-imports.d.ts +4 -0
- package/src/cli/AGENTS.md +39 -0
- package/src/cli/runner.ts +148 -14
- package/src/cli.ts +13 -4
- package/src/commands/AGENTS.md +40 -0
- package/src/commands/approve.ts +62 -3
- package/src/commands/auth.ts +167 -25
- package/src/commands/chat.ts +37 -8
- package/src/commands/deep-interview.ts +633 -175
- package/src/commands/doctor.ts +84 -37
- package/src/commands/evolve-core.ts +18 -0
- package/src/commands/evolve.ts +2 -1
- package/src/commands/export.ts +176 -0
- package/src/commands/gjc.ts +52 -0
- package/src/commands/launch.ts +3549 -240
- package/src/commands/mcp.ts +3 -3
- package/src/commands/ooo-seed.ts +19 -0
- package/src/commands/ralplan.ts +253 -35
- package/src/commands/resume.ts +1 -1
- package/src/commands/session.ts +183 -0
- package/src/commands/setup-helpers.ts +10 -3
- package/src/commands/setup.ts +57 -16
- package/src/commands/skills.ts +78 -18
- package/src/commands/state.ts +198 -0
- package/src/commands/status.ts +84 -0
- package/src/commands/team.ts +340 -212
- package/src/commands/ultragoal.ts +122 -61
- package/src/commands/update.ts +244 -0
- package/src/ledger.ts +270 -0
- package/src/mcp/AGENTS.md +38 -0
- package/src/mcp/server.ts +115 -14
- package/src/mcp/tools.ts +42 -22
- package/src/md-modules.d.ts +4 -0
- package/src/prompts/AGENTS.md +41 -0
- package/src/prompts/agents/AGENTS.md +35 -0
- package/src/prompts/agents/architect.md +35 -0
- package/src/prompts/agents/critic.md +37 -0
- package/src/prompts/agents/executor.md +36 -0
- package/src/prompts/agents/planner.md +37 -0
- package/src/prompts/skills/AGENTS.md +36 -0
- package/src/prompts/skills/deep-dive/AGENTS.md +31 -0
- package/src/prompts/skills/deep-dive/SKILL.md +13 -0
- package/src/prompts/skills/deep-interview/AGENTS.md +31 -0
- package/src/prompts/skills/deep-interview/SKILL.md +12 -0
- package/src/prompts/skills/gjc/AGENTS.md +31 -0
- package/src/prompts/skills/gjc/SKILL.md +15 -0
- package/src/prompts/skills/ralplan/AGENTS.md +31 -0
- package/src/prompts/skills/ralplan/SKILL.md +11 -0
- package/src/prompts/skills/team/AGENTS.md +31 -0
- package/src/prompts/skills/team/SKILL.md +11 -0
- package/src/prompts/skills/ultragoal/AGENTS.md +31 -0
- package/src/prompts/skills/ultragoal/SKILL.md +11 -0
- package/src/skills/AGENTS.md +38 -0
- package/src/skills/catalog.ts +565 -31
- package/src/tui/AGENTS.md +43 -0
- package/src/tui/app.ts +1181 -92
- package/src/tui/components/AGENTS.md +42 -0
- package/src/tui/components/ascii-art.ts +257 -15
- package/src/tui/components/autocomplete.ts +98 -16
- package/src/tui/components/autopilot-status.ts +65 -0
- package/src/tui/components/category-index.ts +49 -0
- package/src/tui/components/code-view.ts +54 -11
- package/src/tui/components/color.ts +171 -2
- package/src/tui/components/config-panel.ts +82 -15
- package/src/tui/components/duration.ts +38 -0
- package/src/tui/components/evolution.ts +3 -3
- package/src/tui/components/footer.ts +91 -42
- package/src/tui/components/forge.ts +426 -31
- package/src/tui/components/hints.ts +54 -0
- package/src/tui/components/hud.ts +73 -0
- package/src/tui/components/index.ts +4 -0
- package/src/tui/components/input-box.ts +150 -0
- package/src/tui/components/layout.ts +11 -3
- package/src/tui/components/live-model-picker.ts +108 -0
- package/src/tui/components/markdown-table.ts +140 -0
- package/src/tui/components/markdown-text.ts +97 -0
- package/src/tui/components/meter.ts +4 -1
- package/src/tui/components/model-picker.ts +3 -2
- package/src/tui/components/provider-picker.ts +3 -2
- package/src/tui/components/section.ts +70 -0
- package/src/tui/components/select-list.ts +40 -10
- package/src/tui/components/skill-picker.ts +25 -0
- package/src/tui/components/slash.ts +244 -21
- package/src/tui/components/status.ts +272 -11
- package/src/tui/components/step-timeline.ts +218 -0
- package/src/tui/components/stream.ts +26 -9
- package/src/tui/components/themes.ts +212 -6
- package/src/tui/components/todo-card.ts +47 -0
- package/src/tui/components/tool-list.ts +58 -12
- package/src/tui/components/transcript.ts +120 -0
- package/src/tui/components/update-box.ts +31 -0
- package/src/tui/components/welcome.ts +162 -0
- package/src/tui/components/width.ts +163 -0
- package/src/tui/monitoring/AGENTS.md +31 -0
- package/src/tui/monitoring/hud-view.ts +55 -0
- package/src/tui/renderer.ts +112 -3
- package/src/tui/terminal.ts +40 -33
- package/src/util/AGENTS.md +39 -0
- package/src/util/clipboard-image.ts +118 -0
- package/src/util/env.ts +12 -0
- package/src/util/provider-error.ts +78 -0
- package/src/util/retry.ts +91 -6
- package/src/util/update-check.ts +64 -0
- 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
|
-
*
|
|
4
|
-
* provider, how it will authenticate (API key / OAuth / keyless / none),
|
|
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 {
|
|
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 '
|
|
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 '
|
|
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
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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?:
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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",
|