Skip to content

Commit 99a5232

Browse files
rootmultica-agent
andcommitted
feat(omp-claude): synchronize Claude OAuth extension and transport optimizations
Co-authored-by: multica-agent <github@multica.ai>
1 parent d4fe952 commit 99a5232

15 files changed

Lines changed: 3722 additions & 0 deletions
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
/**
2+
* Claude Provider Extension for Pi Agent.
3+
*
4+
* Provides native, zero-stall, ultra-low-latency streaming for:
5+
* - (oAuth) Claude Opus 5
6+
* - (oAuth) Claude Opus 4.8
7+
* - (oAuth) Claude Sonnet 5
8+
* - (oAuth) Claude Sonnet 4.6
9+
* - (oAuth) Claude Haiku 4.5
10+
*
11+
* Supports Dual-Seam Authentication:
12+
* 1. Claude Code Enterprise OAuth Session
13+
* 2. Direct ANTHROPIC_API_KEY environment variable fallback
14+
*/
15+
16+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17+
import { ClaudeClient, fetchClaudeQuota, formatClaudeQuotaSnapshot, type ClaudeModelSpec } from "../lib/claude/index.js";
18+
import { ProviderQuotaStore } from "../lib/common/quota-store.js";
19+
20+
const PREFERRED_MODEL_ID = "claude-opus-5";
21+
const ANTHROPIC_OVERFLOW_PATTERN = /prompt is too long|exceeds the maximum context length|max_tokens exceeds|context length exceeded|prompt exceeds model context window/i;
22+
23+
export const CLAUDE_MODELS: ClaudeModelSpec[] = [
24+
{
25+
id: "claude-fable-5",
26+
name: "(oAuth) Claude Fable 5",
27+
backend: "claude-fable-5",
28+
effort: "max",
29+
thinkingBudgetTokens: 64000,
30+
maxTokens: 128000,
31+
contextWindow: 1000000,
32+
supportsAdaptiveThinking: true,
33+
supportsStrictTools: true,
34+
supportsEffort: true,
35+
},
36+
{
37+
id: "claude-opus-5",
38+
name: "(oAuth) Claude Opus 5",
39+
backend: "claude-opus-5",
40+
effort: "max",
41+
thinkingBudgetTokens: 64000,
42+
maxTokens: 128000,
43+
contextWindow: 1000000,
44+
supportsAdaptiveThinking: true,
45+
supportsStrictTools: true,
46+
supportsEffort: true,
47+
},
48+
{
49+
id: "claude-opus-4-8",
50+
name: "(oAuth) Claude Opus 4.8",
51+
backend: "claude-opus-4-8",
52+
effort: "max",
53+
thinkingBudgetTokens: 64000,
54+
maxTokens: 128000,
55+
contextWindow: 1000000,
56+
supportsAdaptiveThinking: true,
57+
supportsStrictTools: true,
58+
supportsEffort: true,
59+
},
60+
{
61+
id: "claude-opus-4-6",
62+
name: "(oAuth) Claude Opus 4.6",
63+
backend: "claude-opus-4-6",
64+
effort: "high",
65+
thinkingBudgetTokens: 64000,
66+
maxTokens: 128000,
67+
contextWindow: 1000000,
68+
supportsAdaptiveThinking: true,
69+
supportsStrictTools: true,
70+
supportsEffort: true,
71+
},
72+
{
73+
id: "claude-sonnet-5",
74+
name: "(oAuth) Claude Sonnet 5",
75+
backend: "claude-sonnet-5",
76+
effort: "max",
77+
thinkingBudgetTokens: 64000,
78+
maxTokens: 128000,
79+
contextWindow: 1000000,
80+
supportsAdaptiveThinking: true,
81+
supportsStrictTools: true,
82+
supportsEffort: true,
83+
},
84+
{
85+
id: "claude-sonnet-4-6",
86+
name: "(oAuth) Claude Sonnet 4.6",
87+
backend: "claude-sonnet-4-6",
88+
effort: "high",
89+
thinkingBudgetTokens: 16384,
90+
maxTokens: 128000,
91+
contextWindow: 200000,
92+
supportsAdaptiveThinking: true,
93+
supportsStrictTools: true,
94+
supportsEffort: true,
95+
},
96+
{
97+
id: "claude-haiku-4-5",
98+
name: "(oAuth) Claude Haiku 4.5",
99+
backend: "claude-haiku-4-5-20251001",
100+
effort: "off",
101+
thinkingBudgetTokens: 0,
102+
maxTokens: 64000,
103+
contextWindow: 200000,
104+
supportsAdaptiveThinking: false,
105+
supportsStrictTools: true,
106+
supportsEffort: false,
107+
},
108+
];
109+
110+
function modelById(id: string): ClaudeModelSpec | undefined {
111+
return CLAUDE_MODELS.find((m) => m.id === id);
112+
}
113+
114+
export default async function claudeExtension(pi: ExtensionAPI) {
115+
const client = new ClaudeClient();
116+
117+
const providerConfig = {
118+
name: "Claude (OAuth)",
119+
baseUrl: "https://api.anthropic.com",
120+
apiKey: "claude-auth",
121+
api: "claude-custom",
122+
streamSimple: (model: any, context: any, options: any) => {
123+
const spec = modelById(model.id);
124+
if (!spec) throw new Error(`Unknown Claude model: ${model.id}`);
125+
return client.stream(model, spec, context, options);
126+
},
127+
models: CLAUDE_MODELS.map((m) => ({
128+
id: m.id,
129+
name: m.name,
130+
reasoning: m.supportsAdaptiveThinking || (m.thinkingBudgetTokens ?? 0) > 0,
131+
input: ["text", "image"] as ("text" | "image")[],
132+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
133+
contextWindow: m.contextWindow,
134+
maxTokens: m.maxTokens,
135+
thinkingLevelMap: m.supportsAdaptiveThinking || (m.thinkingBudgetTokens ?? 0) > 0
136+
? {
137+
off: null, // Claude thinking models have no "off"
138+
minimal: "low", // 1. low
139+
low: "medium", // 2. medium
140+
medium: "high", // 3. high
141+
high: "xhigh", // 4. xhigh
142+
xhigh: "max", // 5. max
143+
max: "ultracode", // 6. ultracode
144+
}
145+
: {
146+
off: "off",
147+
minimal: null,
148+
low: null,
149+
medium: null,
150+
high: null,
151+
xhigh: null,
152+
max: null,
153+
},
154+
})),
155+
};
156+
157+
// Register under primary "oauth" (so status bar shows `(oAuth)`) and override built-in "anthropic"
158+
pi.registerProvider("oauth", providerConfig);
159+
pi.registerProvider("anthropic", {
160+
...providerConfig,
161+
name: "Anthropic (OAuth)",
162+
});
163+
164+
// Normalize context overflow errors for Pi automatic compaction
165+
pi.on("message_end", (event, ctx) => {
166+
const message = event.message;
167+
if (message.role !== "assistant" || message.stopReason !== "error") return;
168+
const prov = message.provider || ctx.model?.provider;
169+
if (prov !== "oauth" && prov !== "claude" && prov !== "anthropic") return;
170+
171+
const errorMessage = message.errorMessage ?? "";
172+
if (errorMessage.includes("context_length_exceeded")) return;
173+
if (!ANTHROPIC_OVERFLOW_PATTERN.test(errorMessage)) return;
174+
175+
return {
176+
message: {
177+
...message,
178+
errorMessage: `context_length_exceeded: ${errorMessage}`,
179+
},
180+
};
181+
});
182+
183+
const handleCommand = async (args: string, ctx: any, prefix: string) => {
184+
const sub = (args || "").trim().split(/\s+/)[0] || "status";
185+
186+
if (sub === "models") {
187+
const lines = CLAUDE_MODELS.map((m) => {
188+
const mark = m.id === PREFERRED_MODEL_ID ? " <- default pick" : "";
189+
return ` ${m.id} (${m.name})\n backend: ${m.backend} | default effort: ${m.effort} | maxTokens: ${m.maxTokens}${mark}`;
190+
}).join("\n");
191+
ctx.ui.notify(`Claude OAuth Models (Native Stream):\n${lines}`, "info");
192+
return;
193+
}
194+
195+
if (sub === "quota" || sub === "usage") {
196+
ctx.ui.notify("Fetching live Claude quota telemetry…", "info");
197+
const snapshot = await fetchClaudeQuota();
198+
ctx.ui.notify(formatClaudeQuotaSnapshot(snapshot), snapshot.ok ? "info" : "warn");
199+
return;
200+
}
201+
202+
if (sub === "auth" || sub === "status") {
203+
const status = await client.getStatus(PREFERRED_MODEL_ID);
204+
if (!status.connected) {
205+
ctx.ui.notify(
206+
`Claude Auth: NOT logged in (${status.error ?? "No active session"}).\nRun \`claude auth login\` in a terminal, then /reload.`,
207+
"warn",
208+
);
209+
return;
210+
}
211+
const remainingStr = status.tokenRemainingMinutes !== undefined
212+
? ` (${status.tokenRemainingMinutes}m until auto-refresh)`
213+
: "";
214+
215+
const q = ProviderQuotaStore.get().getQuota("claude");
216+
const quotaInfo = q?.ok
217+
? `\n• Live Quota: 5h:${q.fiveHourRemainingPct}% remaining • 7d:${q.weeklyRemainingPct}% remaining`
218+
: "";
219+
220+
ctx.ui.notify(
221+
`Claude Connected (Zero-Subprocess Native Stream):\n` +
222+
`• Provider: (oAuth)\n` +
223+
`• Auth Mode: ${status.authMode.toUpperCase()}${remainingStr}\n` +
224+
`• Preferred Model: ${PREFERRED_MODEL_ID} (128k output, 1M context)\n` +
225+
`• Reasoning Engine: Adaptive thinking with Shift+Tab effort control\n` +
226+
`• Caching: 4-Tier Ephemeral Prompt Caching Enabled (1-hour TTL)\n` +
227+
`• Plan: EBU Claude Enterprise ($0 API Cost)${quotaInfo}`,
228+
"info",
229+
);
230+
return;
231+
}
232+
233+
if (sub === "login") {
234+
ctx.ui.notify("To sign in, run `claude auth login` in any terminal pane and authorize with EBU Claude, then /reload.", "info");
235+
return;
236+
}
237+
238+
ctx.ui.notify(`Usage: /${prefix} <status|models|quota|usage|auth|login>`, "info");
239+
};
240+
241+
pi.registerCommand("oauth", {
242+
description: "Claude OAuth commands: status | models | quota | auth | login",
243+
handler: async (args, ctx) => handleCommand(args, ctx, "oauth"),
244+
});
245+
246+
pi.registerCommand("claude", {
247+
description: "Claude OAuth commands: status | models | quota | auth | login",
248+
handler: async (args, ctx) => handleCommand(args, ctx, "claude"),
249+
});
250+
}

0 commit comments

Comments
 (0)