@otto-assistant/bridge 0.4.100 → 0.4.101

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.
@@ -22,71 +22,72 @@
22
22
  * - https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/anthropic.ts
23
23
  * - https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts
24
24
  */
25
- import { loadAccountStore, rememberAnthropicOAuth, rotateAnthropicAccount, saveAccountStore, setAnthropicAuth, shouldRotateAuth, upsertAccount, withAuthStateLock, } from './anthropic-auth-state.js';
26
- import { extractAnthropicAccountIdentity, } from './anthropic-account-identity.js';
25
+ import { loadAccountStore, rememberAnthropicOAuth, rotateAnthropicAccount, saveAccountStore, setAnthropicAuth, shouldRotateAuth, upsertAccount, withAuthStateLock, } from "./anthropic-auth-state.js";
26
+ import { extractAnthropicAccountIdentity, } from "./anthropic-account-identity.js";
27
27
  // PKCE (Proof Key for Code Exchange) using Web Crypto API.
28
28
  // Reference: https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/pkce.ts
29
29
  function base64urlEncode(bytes) {
30
- let binary = '';
30
+ let binary = "";
31
31
  for (const byte of bytes) {
32
32
  binary += String.fromCharCode(byte);
33
33
  }
34
- return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
34
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
35
35
  }
36
36
  async function generatePKCE() {
37
37
  const verifierBytes = new Uint8Array(32);
38
38
  crypto.getRandomValues(verifierBytes);
39
39
  const verifier = base64urlEncode(verifierBytes);
40
40
  const data = new TextEncoder().encode(verifier);
41
- const hashBuffer = await crypto.subtle.digest('SHA-256', data);
41
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
42
42
  const challenge = base64urlEncode(new Uint8Array(hashBuffer));
43
43
  return { verifier, challenge };
44
44
  }
45
- import { spawn } from 'node:child_process';
46
- import { createServer } from 'node:http';
45
+ import { spawn } from "node:child_process";
46
+ import { createServer } from "node:http";
47
47
  // --- Constants ---
48
48
  const CLIENT_ID = (() => {
49
- const encoded = 'OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl';
50
- return typeof atob === 'function'
49
+ const encoded = "OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl";
50
+ return typeof atob === "function"
51
51
  ? atob(encoded)
52
- : Buffer.from(encoded, 'base64').toString('utf8');
52
+ : Buffer.from(encoded, "base64").toString("utf8");
53
53
  })();
54
- const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
55
- const CREATE_API_KEY_URL = 'https://api.anthropic.com/api/oauth/claude_cli/create_api_key';
56
- const CLIENT_DATA_URL = 'https://api.anthropic.com/api/oauth/claude_cli/client_data';
57
- const PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile';
54
+ const TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
55
+ const CREATE_API_KEY_URL = "https://api.anthropic.com/api/oauth/claude_cli/create_api_key";
56
+ const CLIENT_DATA_URL = "https://api.anthropic.com/api/oauth/claude_cli/client_data";
57
+ const PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
58
58
  const CALLBACK_PORT = 53692;
59
- const CALLBACK_PATH = '/callback';
59
+ const CALLBACK_PATH = "/callback";
60
60
  const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
61
- const SCOPES = 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload';
61
+ const SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
62
62
  const OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
63
- const CLAUDE_CODE_VERSION = '2.1.75';
63
+ const CLAUDE_CODE_VERSION = "2.1.75";
64
64
  const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
65
- const OPENCODE_IDENTITY = 'You are OpenCode, the best coding agent on the planet.';
66
- const CLAUDE_CODE_BETA = 'claude-code-20250219';
67
- const OAUTH_BETA = 'oauth-2025-04-20';
68
- const FINE_GRAINED_TOOL_STREAMING_BETA = 'fine-grained-tool-streaming-2025-05-14';
69
- const INTERLEAVED_THINKING_BETA = 'interleaved-thinking-2025-05-14';
70
- const TOAST_SESSION_HEADER = 'x-kimaki-session-id';
65
+ const OPENCODE_IDENTITY = "You are OpenCode, the best coding agent on the planet.";
66
+ const ANTHROPIC_PROMPT_MARKER = "Skills provide specialized instructions";
67
+ const CLAUDE_CODE_BETA = "claude-code-20250219";
68
+ const OAUTH_BETA = "oauth-2025-04-20";
69
+ const FINE_GRAINED_TOOL_STREAMING_BETA = "fine-grained-tool-streaming-2025-05-14";
70
+ const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
71
+ const TOAST_SESSION_HEADER = "x-kimaki-session-id";
71
72
  const ANTHROPIC_HOSTS = new Set([
72
- 'api.anthropic.com',
73
- 'claude.ai',
74
- 'console.anthropic.com',
75
- 'platform.claude.com',
73
+ "api.anthropic.com",
74
+ "claude.ai",
75
+ "console.anthropic.com",
76
+ "platform.claude.com",
76
77
  ]);
77
78
  const OPENCODE_TO_CLAUDE_CODE_TOOL_NAME = {
78
- bash: 'Bash',
79
- edit: 'Edit',
80
- glob: 'Glob',
81
- grep: 'Grep',
82
- question: 'AskUserQuestion',
83
- read: 'Read',
84
- skill: 'Skill',
85
- task: 'Task',
86
- todowrite: 'TodoWrite',
87
- webfetch: 'WebFetch',
88
- websearch: 'WebSearch',
89
- write: 'Write',
79
+ bash: "Bash",
80
+ edit: "Edit",
81
+ glob: "Glob",
82
+ grep: "Grep",
83
+ question: "AskUserQuestion",
84
+ read: "Read",
85
+ skill: "Skill",
86
+ task: "Task",
87
+ todowrite: "TodoWrite",
88
+ webfetch: "WebFetch",
89
+ websearch: "WebSearch",
90
+ write: "Write",
90
91
  };
91
92
  // --- HTTP helpers ---
92
93
  // Claude OAuth token exchange can 429 when this runs inside the opencode auth
@@ -101,8 +102,8 @@ async function requestText(urlString, options) {
101
102
  method: options.method,
102
103
  url: urlString,
103
104
  });
104
- const child = spawn('node', [
105
- '-e',
105
+ const child = spawn("node", [
106
+ "-e",
106
107
  `
107
108
  const input = JSON.parse(process.argv[1]);
108
109
  (async () => {
@@ -124,32 +125,32 @@ const input = JSON.parse(process.argv[1]);
124
125
  `.trim(),
125
126
  payload,
126
127
  ], {
127
- stdio: ['ignore', 'pipe', 'pipe'],
128
+ stdio: ["ignore", "pipe", "pipe"],
128
129
  });
129
- let stdout = '';
130
- let stderr = '';
130
+ let stdout = "";
131
+ let stderr = "";
131
132
  const timeout = setTimeout(() => {
132
133
  child.kill();
133
134
  reject(new Error(`Request timed out. url=${urlString}`));
134
135
  }, 30_000);
135
- child.stdout.on('data', (chunk) => {
136
+ child.stdout.on("data", (chunk) => {
136
137
  stdout += String(chunk);
137
138
  });
138
- child.stderr.on('data', (chunk) => {
139
+ child.stderr.on("data", (chunk) => {
139
140
  stderr += String(chunk);
140
141
  });
141
- child.on('error', (error) => {
142
+ child.on("error", (error) => {
142
143
  clearTimeout(timeout);
143
144
  reject(error);
144
145
  });
145
- child.on('close', (code) => {
146
+ child.on("close", (code) => {
146
147
  clearTimeout(timeout);
147
148
  if (code !== 0) {
148
149
  let details = stderr.trim();
149
150
  try {
150
151
  const parsed = JSON.parse(details);
151
- if (typeof parsed.status === 'number') {
152
- reject(new Error(`HTTP ${parsed.status} from ${urlString}: ${parsed.body ?? ''}`));
152
+ if (typeof parsed.status === "number") {
153
+ reject(new Error(`HTTP ${parsed.status} from ${urlString}: ${parsed.body ?? ""}`));
153
154
  return;
154
155
  }
155
156
  }
@@ -166,11 +167,11 @@ const input = JSON.parse(process.argv[1]);
166
167
  async function postJson(url, body) {
167
168
  const requestBody = JSON.stringify(body);
168
169
  const responseText = await requestText(url, {
169
- method: 'POST',
170
+ method: "POST",
170
171
  headers: {
171
- Accept: 'application/json',
172
- 'Content-Length': String(Buffer.byteLength(requestBody)),
173
- 'Content-Type': 'application/json',
172
+ Accept: "application/json",
173
+ "Content-Length": String(Buffer.byteLength(requestBody)),
174
+ "Content-Type": "application/json",
174
175
  },
175
176
  body: requestBody,
176
177
  });
@@ -190,7 +191,7 @@ function tokenExpiry(expiresIn) {
190
191
  }
191
192
  async function exchangeAuthorizationCode(code, state, verifier, redirectUri) {
192
193
  const json = await postJson(TOKEN_URL, {
193
- grant_type: 'authorization_code',
194
+ grant_type: "authorization_code",
194
195
  client_id: CLIENT_ID,
195
196
  code,
196
197
  state,
@@ -199,7 +200,7 @@ async function exchangeAuthorizationCode(code, state, verifier, redirectUri) {
199
200
  });
200
201
  const data = parseTokenResponse(json);
201
202
  return {
202
- type: 'success',
203
+ type: "success",
203
204
  refresh: data.refresh_token,
204
205
  access: data.access_token,
205
206
  expires: tokenExpiry(data.expires_in),
@@ -207,13 +208,13 @@ async function exchangeAuthorizationCode(code, state, verifier, redirectUri) {
207
208
  }
208
209
  async function refreshAnthropicToken(refreshToken) {
209
210
  const json = await postJson(TOKEN_URL, {
210
- grant_type: 'refresh_token',
211
+ grant_type: "refresh_token",
211
212
  client_id: CLIENT_ID,
212
213
  refresh_token: refreshToken,
213
214
  });
214
215
  const data = parseTokenResponse(json);
215
216
  return {
216
- type: 'oauth',
217
+ type: "oauth",
217
218
  refresh: data.refresh_token,
218
219
  access: data.access_token,
219
220
  expires: tokenExpiry(data.expires_in),
@@ -221,26 +222,27 @@ async function refreshAnthropicToken(refreshToken) {
221
222
  }
222
223
  async function createApiKey(accessToken) {
223
224
  const responseText = await requestText(CREATE_API_KEY_URL, {
224
- method: 'POST',
225
+ method: "POST",
225
226
  headers: {
226
- Accept: 'application/json',
227
+ Accept: "application/json",
227
228
  authorization: `Bearer ${accessToken}`,
228
- 'Content-Type': 'application/json',
229
+ "Content-Type": "application/json",
229
230
  },
230
231
  });
231
232
  const json = JSON.parse(responseText);
232
- return { type: 'success', key: json.raw_key };
233
+ return { type: "success", key: json.raw_key };
233
234
  }
234
235
  async function fetchAnthropicAccountIdentity(accessToken) {
235
236
  const urls = [CLIENT_DATA_URL, PROFILE_URL];
236
237
  for (const url of urls) {
237
238
  const responseText = await requestText(url, {
238
- method: 'GET',
239
+ method: "GET",
239
240
  headers: {
240
- Accept: 'application/json',
241
+ Accept: "application/json",
241
242
  authorization: `Bearer ${accessToken}`,
242
- 'user-agent': process.env.OPENCODE_ANTHROPIC_USER_AGENT || `claude-cli/${CLAUDE_CODE_VERSION}`,
243
- 'x-app': 'cli',
243
+ "user-agent": process.env.OPENCODE_ANTHROPIC_USER_AGENT ||
244
+ `claude-cli/${CLAUDE_CODE_VERSION}`,
245
+ "x-app": "cli",
244
246
  },
245
247
  }).catch(() => {
246
248
  return undefined;
@@ -268,29 +270,31 @@ async function startCallbackServer(expectedState) {
268
270
  });
269
271
  const server = createServer((req, res) => {
270
272
  try {
271
- const url = new URL(req.url || '', 'http://localhost');
273
+ const url = new URL(req.url || "", "http://localhost");
272
274
  if (url.pathname !== CALLBACK_PATH) {
273
- res.writeHead(404).end('Not found');
275
+ res.writeHead(404).end("Not found");
274
276
  return;
275
277
  }
276
- const code = url.searchParams.get('code');
277
- const state = url.searchParams.get('state');
278
- const error = url.searchParams.get('error');
278
+ const code = url.searchParams.get("code");
279
+ const state = url.searchParams.get("state");
280
+ const error = url.searchParams.get("error");
279
281
  if (error || !code || !state || state !== expectedState) {
280
- res.writeHead(400).end('Authentication failed: ' + (error || 'missing code/state'));
282
+ res
283
+ .writeHead(400)
284
+ .end("Authentication failed: " + (error || "missing code/state"));
281
285
  return;
282
286
  }
283
287
  res
284
- .writeHead(200, { 'Content-Type': 'text/plain' })
285
- .end('Authentication successful. You can close this window.');
288
+ .writeHead(200, { "Content-Type": "text/plain" })
289
+ .end("Authentication successful. You can close this window.");
286
290
  settle?.({ code, state });
287
291
  }
288
292
  catch {
289
- res.writeHead(500).end('Internal error');
293
+ res.writeHead(500).end("Internal error");
290
294
  }
291
295
  });
292
- server.once('error', reject);
293
- server.listen(CALLBACK_PORT, '127.0.0.1', () => {
296
+ server.once("error", reject);
297
+ server.listen(CALLBACK_PORT, "127.0.0.1", () => {
294
298
  resolve({
295
299
  server,
296
300
  cancelWait: () => {
@@ -315,13 +319,13 @@ async function beginAuthorizationFlow() {
315
319
  const pkce = await generatePKCE();
316
320
  const callbackServer = await startCallbackServer(pkce.verifier);
317
321
  const authParams = new URLSearchParams({
318
- code: 'true',
322
+ code: "true",
319
323
  client_id: CLIENT_ID,
320
- response_type: 'code',
324
+ response_type: "code",
321
325
  redirect_uri: REDIRECT_URI,
322
326
  scope: SCOPES,
323
327
  code_challenge: pkce.challenge,
324
- code_challenge_method: 'S256',
328
+ code_challenge_method: "S256",
325
329
  state: pkce.verifier,
326
330
  });
327
331
  return {
@@ -358,7 +362,7 @@ async function waitForCallback(callbackServer, manualInput) {
358
362
  }),
359
363
  ]);
360
364
  if (!result?.code) {
361
- throw new Error('Timed out waiting for OAuth callback');
365
+ throw new Error("Timed out waiting for OAuth callback");
362
366
  }
363
367
  return result;
364
368
  }
@@ -370,25 +374,25 @@ async function waitForCallback(callbackServer, manualInput) {
370
374
  function parseManualInput(input) {
371
375
  try {
372
376
  const url = new URL(input);
373
- const code = url.searchParams.get('code');
374
- const state = url.searchParams.get('state');
377
+ const code = url.searchParams.get("code");
378
+ const state = url.searchParams.get("state");
375
379
  if (code)
376
- return { code, state: state || '' };
380
+ return { code, state: state || "" };
377
381
  }
378
382
  catch {
379
383
  // not a URL
380
384
  }
381
- if (input.includes('#')) {
382
- const [code = '', state = ''] = input.split('#', 2);
385
+ if (input.includes("#")) {
386
+ const [code = "", state = ""] = input.split("#", 2);
383
387
  return { code, state };
384
388
  }
385
- if (input.includes('code=')) {
389
+ if (input.includes("code=")) {
386
390
  const params = new URLSearchParams(input);
387
- const code = params.get('code');
391
+ const code = params.get("code");
388
392
  if (code)
389
- return { code, state: params.get('state') || '' };
393
+ return { code, state: params.get("state") || "" };
390
394
  }
391
- return { code: input, state: '' };
395
+ return { code: input, state: "" };
392
396
  }
393
397
  // Unified authorize handler: returns either OAuth tokens or an API key,
394
398
  // for both auto and remote-first modes.
@@ -400,12 +404,12 @@ function buildAuthorizeHandler(mode) {
400
404
  const finalize = async (result) => {
401
405
  const verifier = auth.verifier;
402
406
  const creds = await exchangeAuthorizationCode(result.code, result.state || verifier, verifier, REDIRECT_URI);
403
- if (mode === 'apikey') {
407
+ if (mode === "apikey") {
404
408
  return createApiKey(creds.access);
405
409
  }
406
410
  const identity = await fetchAnthropicAccountIdentity(creds.access);
407
411
  await rememberAnthropicOAuth({
408
- type: 'oauth',
412
+ type: "oauth",
409
413
  refresh: creds.refresh,
410
414
  access: creds.access,
411
415
  expires: creds.expires,
@@ -415,8 +419,8 @@ function buildAuthorizeHandler(mode) {
415
419
  if (!isRemote) {
416
420
  return {
417
421
  url: auth.url,
418
- instructions: 'Complete login in your browser on this machine. OpenCode will catch the localhost callback automatically.',
419
- method: 'auto',
422
+ instructions: "Complete login in your browser on this machine. OpenCode will catch the localhost callback automatically.",
423
+ method: "auto",
420
424
  callback: async () => {
421
425
  pendingAuthResult ??= (async () => {
422
426
  try {
@@ -424,7 +428,7 @@ function buildAuthorizeHandler(mode) {
424
428
  return await finalize(result);
425
429
  }
426
430
  catch {
427
- return { type: 'failed' };
431
+ return { type: "failed" };
428
432
  }
429
433
  })();
430
434
  return pendingAuthResult;
@@ -433,8 +437,8 @@ function buildAuthorizeHandler(mode) {
433
437
  }
434
438
  return {
435
439
  url: auth.url,
436
- instructions: 'Complete login in your browser, then paste the final redirect URL from the address bar here. Pasting just the authorization code also works.',
437
- method: 'code',
440
+ instructions: "Complete login in your browser, then paste the final redirect URL from the address bar here. Pasting just the authorization code also works.",
441
+ method: "code",
438
442
  callback: async (input) => {
439
443
  pendingAuthResult ??= (async () => {
440
444
  try {
@@ -442,7 +446,7 @@ function buildAuthorizeHandler(mode) {
442
446
  return await finalize(result);
443
447
  }
444
448
  catch {
445
- return { type: 'failed' };
449
+ return { type: "failed" };
446
450
  }
447
451
  })();
448
452
  return pendingAuthResult;
@@ -456,48 +460,56 @@ function buildAuthorizeHandler(mode) {
456
460
  function toClaudeCodeToolName(name) {
457
461
  return OPENCODE_TO_CLAUDE_CODE_TOOL_NAME[name.toLowerCase()] ?? name;
458
462
  }
459
- function sanitizeSystemText(text, onError) {
463
+ function sanitizeAnthropicSystemText(text, onError) {
460
464
  const startIdx = text.indexOf(OPENCODE_IDENTITY);
461
465
  if (startIdx === -1)
462
466
  return text;
463
- // to find the last heading to match readhttps://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/session/prompt/anthropic.txt
464
- // it contains the opencode injected prompt. you must keep the codeRefsMarker updated with that package
465
- const codeRefsMarker = '# Code References';
466
- const endIdx = text.indexOf(codeRefsMarker, startIdx);
467
+ // Keep the marker aligned with the current OpenCode Anthropic prompt.
468
+ const endIdx = text.indexOf(ANTHROPIC_PROMPT_MARKER, startIdx);
467
469
  if (endIdx === -1) {
468
- onError?.(`sanitizeSystemText: could not find '# Code References' after OpenCode identity`);
470
+ onError?.("sanitizeAnthropicSystemText: could not find Anthropic prompt marker after OpenCode identity");
469
471
  return text;
470
472
  }
471
- // Remove everything from the OpenCode identity up to (but not including) '# Code References'
472
- return text.slice(0, startIdx) + text.slice(endIdx);
473
+ return (text.slice(0, startIdx) + text.slice(endIdx)).replaceAll("opencode", "openc0de");
474
+ }
475
+ function mapSystemTextPart(part, onError) {
476
+ if (typeof part === "string") {
477
+ return { type: "text", text: sanitizeAnthropicSystemText(part, onError) };
478
+ }
479
+ if (part &&
480
+ typeof part === "object" &&
481
+ "type" in part &&
482
+ part.type === "text" &&
483
+ "text" in part &&
484
+ typeof part.text === "string") {
485
+ return {
486
+ ...part,
487
+ text: sanitizeAnthropicSystemText(part.text, onError),
488
+ };
489
+ }
490
+ return part;
473
491
  }
474
492
  function prependClaudeCodeIdentity(system, onError) {
475
- const identityBlock = { type: 'text', text: CLAUDE_CODE_IDENTITY };
476
- if (typeof system === 'undefined')
493
+ const identityBlock = { type: "text", text: CLAUDE_CODE_IDENTITY };
494
+ if (typeof system === "undefined")
477
495
  return [identityBlock];
478
- if (typeof system === 'string') {
479
- const sanitized = sanitizeSystemText(system, onError);
496
+ if (typeof system === "string") {
497
+ const sanitized = sanitizeAnthropicSystemText(system, onError);
480
498
  if (sanitized === CLAUDE_CODE_IDENTITY)
481
499
  return [identityBlock];
482
- return [identityBlock, { type: 'text', text: sanitized }];
500
+ return [identityBlock, { type: "text", text: sanitized }];
483
501
  }
484
502
  if (!Array.isArray(system))
485
503
  return [identityBlock, system];
486
504
  const sanitized = system.map((item) => {
487
- if (typeof item === 'string')
488
- return { type: 'text', text: sanitizeSystemText(item, onError) };
489
- if (item && typeof item === 'object' && item.type === 'text') {
490
- const text = item.text;
491
- if (typeof text === 'string') {
492
- return { ...item, text: sanitizeSystemText(text, onError) };
493
- }
494
- }
495
- return item;
505
+ return mapSystemTextPart(item, onError);
496
506
  });
497
507
  const first = sanitized[0];
498
508
  if (first &&
499
- typeof first === 'object' &&
500
- first.type === 'text' &&
509
+ typeof first === "object" &&
510
+ "type" in first &&
511
+ first.type === "text" &&
512
+ "text" in first &&
501
513
  first.text === CLAUDE_CODE_IDENTITY) {
502
514
  return sanitized;
503
515
  }
@@ -505,18 +517,22 @@ function prependClaudeCodeIdentity(system, onError) {
505
517
  }
506
518
  function rewriteRequestPayload(body, onError) {
507
519
  if (!body)
508
- return { body, modelId: undefined, reverseToolNameMap: new Map() };
520
+ return {
521
+ body,
522
+ modelId: undefined,
523
+ reverseToolNameMap: new Map(),
524
+ };
509
525
  try {
510
526
  const payload = JSON.parse(body);
511
527
  const reverseToolNameMap = new Map();
512
- const modelId = typeof payload.model === 'string' ? payload.model : undefined;
528
+ const modelId = typeof payload.model === "string" ? payload.model : undefined;
513
529
  // Build reverse map and rename tools
514
530
  if (Array.isArray(payload.tools)) {
515
531
  payload.tools = payload.tools.map((tool) => {
516
- if (!tool || typeof tool !== 'object')
532
+ if (!tool || typeof tool !== "object")
517
533
  return tool;
518
534
  const name = tool.name;
519
- if (typeof name !== 'string')
535
+ if (typeof name !== "string")
520
536
  return tool;
521
537
  const mapped = toClaudeCodeToolName(name);
522
538
  reverseToolNameMap.set(mapped, name);
@@ -527,10 +543,10 @@ function rewriteRequestPayload(body, onError) {
527
543
  payload.system = prependClaudeCodeIdentity(payload.system, onError);
528
544
  // Rename tool_choice
529
545
  if (payload.tool_choice &&
530
- typeof payload.tool_choice === 'object' &&
531
- payload.tool_choice.type === 'tool') {
546
+ typeof payload.tool_choice === "object" &&
547
+ payload.tool_choice.type === "tool") {
532
548
  const name = payload.tool_choice.name;
533
- if (typeof name === 'string') {
549
+ if (typeof name === "string") {
534
550
  payload.tool_choice = {
535
551
  ...payload.tool_choice,
536
552
  name: toClaudeCodeToolName(name),
@@ -540,7 +556,7 @@ function rewriteRequestPayload(body, onError) {
540
556
  // Rename tool_use blocks in messages
541
557
  if (Array.isArray(payload.messages)) {
542
558
  payload.messages = payload.messages.map((message) => {
543
- if (!message || typeof message !== 'object')
559
+ if (!message || typeof message !== "object")
544
560
  return message;
545
561
  const content = message.content;
546
562
  if (!Array.isArray(content))
@@ -548,12 +564,15 @@ function rewriteRequestPayload(body, onError) {
548
564
  return {
549
565
  ...message,
550
566
  content: content.map((block) => {
551
- if (!block || typeof block !== 'object')
567
+ if (!block || typeof block !== "object")
552
568
  return block;
553
569
  const b = block;
554
- if (b.type !== 'tool_use' || typeof b.name !== 'string')
570
+ if (b.type !== "tool_use" || typeof b.name !== "string")
555
571
  return block;
556
- return { ...block, name: toClaudeCodeToolName(b.name) };
572
+ return {
573
+ ...block,
574
+ name: toClaudeCodeToolName(b.name),
575
+ };
557
576
  }),
558
577
  };
559
578
  });
@@ -561,7 +580,11 @@ function rewriteRequestPayload(body, onError) {
561
580
  return { body: JSON.stringify(payload), modelId, reverseToolNameMap };
562
581
  }
563
582
  catch {
564
- return { body, modelId: undefined, reverseToolNameMap: new Map() };
583
+ return {
584
+ body,
585
+ modelId: undefined,
586
+ reverseToolNameMap: new Map(),
587
+ };
565
588
  }
566
589
  }
567
590
  function wrapResponseStream(response, reverseToolNameMap) {
@@ -570,7 +593,7 @@ function wrapResponseStream(response, reverseToolNameMap) {
570
593
  const reader = response.body.getReader();
571
594
  const decoder = new TextDecoder();
572
595
  const encoder = new TextEncoder();
573
- let carry = '';
596
+ let carry = "";
574
597
  const transform = (text) => {
575
598
  return text.replace(/"name"\s*:\s*"([^"]+)"/g, (full, name) => {
576
599
  const original = reverseToolNameMap.get(name);
@@ -613,11 +636,15 @@ function appendToastSessionMarker({ message, sessionId, }) {
613
636
  }
614
637
  // --- Beta headers ---
615
638
  function getRequiredBetas(modelId) {
616
- const betas = [CLAUDE_CODE_BETA, OAUTH_BETA, FINE_GRAINED_TOOL_STREAMING_BETA];
617
- const isAdaptive = modelId?.includes('opus-4-6') ||
618
- modelId?.includes('opus-4.6') ||
619
- modelId?.includes('sonnet-4-6') ||
620
- modelId?.includes('sonnet-4.6');
639
+ const betas = [
640
+ CLAUDE_CODE_BETA,
641
+ OAUTH_BETA,
642
+ FINE_GRAINED_TOOL_STREAMING_BETA,
643
+ ];
644
+ const isAdaptive = modelId?.includes("opus-4-6") ||
645
+ modelId?.includes("opus-4.6") ||
646
+ modelId?.includes("sonnet-4-6") ||
647
+ modelId?.includes("sonnet-4.6");
621
648
  if (!isAdaptive)
622
649
  betas.push(INTERLEAVED_THINKING_BETA);
623
650
  return betas;
@@ -626,16 +653,16 @@ function mergeBetas(existing, required) {
626
653
  return [
627
654
  ...new Set([
628
655
  ...required,
629
- ...(existing || '')
630
- .split(',')
656
+ ...(existing || "")
657
+ .split(",")
631
658
  .map((s) => s.trim())
632
659
  .filter(Boolean),
633
660
  ]),
634
- ].join(',');
661
+ ].join(",");
635
662
  }
636
663
  // --- Token refresh with dedup ---
637
664
  function isOAuthStored(auth) {
638
- return auth.type === 'oauth';
665
+ return auth.type === "oauth";
639
666
  }
640
667
  async function getFreshOAuth(getAuth, client) {
641
668
  const auth = await getAuth();
@@ -650,7 +677,7 @@ async function getFreshOAuth(getAuth, client) {
650
677
  const refreshPromise = withAuthStateLock(async () => {
651
678
  const latest = await getAuth();
652
679
  if (!isOAuthStored(latest)) {
653
- throw new Error('Anthropic OAuth credentials disappeared during refresh');
680
+ throw new Error("Anthropic OAuth credentials disappeared during refresh");
654
681
  }
655
682
  if (latest.access && latest.expires > Date.now())
656
683
  return latest;
@@ -660,7 +687,8 @@ async function getFreshOAuth(getAuth, client) {
660
687
  if (store.accounts.length > 0) {
661
688
  const identity = (() => {
662
689
  const currentIndex = store.accounts.findIndex((account) => {
663
- return account.refresh === latest.refresh || account.access === latest.access;
690
+ return (account.refresh === latest.refresh ||
691
+ account.access === latest.access);
664
692
  });
665
693
  const current = currentIndex >= 0 ? store.accounts[currentIndex] : undefined;
666
694
  if (!current)
@@ -680,27 +708,26 @@ async function getFreshOAuth(getAuth, client) {
680
708
  pendingRefresh.delete(auth.refresh);
681
709
  });
682
710
  }
683
- // --- Plugin export ---
684
711
  const AnthropicAuthPlugin = async ({ client }) => {
685
712
  return {
686
- 'chat.headers': async (input, output) => {
687
- if (input.model.providerID !== 'anthropic') {
713
+ "chat.headers": async (input, output) => {
714
+ if (input.model.providerID !== "anthropic") {
688
715
  return;
689
716
  }
690
717
  output.headers[TOAST_SESSION_HEADER] = input.sessionID;
691
718
  },
692
719
  auth: {
693
- provider: 'anthropic',
720
+ provider: "anthropic",
694
721
  async loader(getAuth, provider) {
695
722
  const auth = await getAuth();
696
- if (auth.type !== 'oauth')
723
+ if (auth.type !== "oauth")
697
724
  return {};
698
725
  // Zero out costs for OAuth users (Claude Pro/Max subscription)
699
726
  for (const model of Object.values(provider.models)) {
700
727
  model.cost = { input: 0, output: 0, cache: { read: 0, write: 0 } };
701
728
  }
702
729
  return {
703
- apiKey: '',
730
+ apiKey: "",
704
731
  async fetch(input, init) {
705
732
  const url = (() => {
706
733
  try {
@@ -712,7 +739,7 @@ const AnthropicAuthPlugin = async ({ client }) => {
712
739
  })();
713
740
  if (!url || !ANTHROPIC_HOSTS.has(url.hostname))
714
741
  return fetch(input, init);
715
- const originalBody = typeof init?.body === 'string'
742
+ const originalBody = typeof init?.body === "string"
716
743
  ? init.body
717
744
  : input instanceof Request
718
745
  ? await input
@@ -729,24 +756,30 @@ const AnthropicAuthPlugin = async ({ client }) => {
729
756
  }
730
757
  const sessionId = headers.get(TOAST_SESSION_HEADER) ?? undefined;
731
758
  const rewritten = rewriteRequestPayload(originalBody, (msg) => {
732
- client.tui.showToast({
759
+ client.tui
760
+ .showToast({
733
761
  body: {
734
- message: appendToastSessionMarker({ message: msg, sessionId }),
735
- variant: 'error',
762
+ message: appendToastSessionMarker({
763
+ message: msg,
764
+ sessionId,
765
+ }),
766
+ variant: "error",
736
767
  },
737
- }).catch(() => { });
768
+ })
769
+ .catch(() => { });
738
770
  });
739
771
  const betas = getRequiredBetas(rewritten.modelId);
740
772
  const runRequest = async (auth) => {
741
773
  const requestHeaders = new Headers(headers);
742
774
  requestHeaders.delete(TOAST_SESSION_HEADER);
743
- requestHeaders.set('accept', 'application/json');
744
- requestHeaders.set('anthropic-beta', mergeBetas(requestHeaders.get('anthropic-beta'), betas));
745
- requestHeaders.set('anthropic-dangerous-direct-browser-access', 'true');
746
- requestHeaders.set('authorization', `Bearer ${auth.access}`);
747
- requestHeaders.set('user-agent', process.env.OPENCODE_ANTHROPIC_USER_AGENT || `claude-cli/${CLAUDE_CODE_VERSION}`);
748
- requestHeaders.set('x-app', 'cli');
749
- requestHeaders.delete('x-api-key');
775
+ requestHeaders.set("accept", "application/json");
776
+ requestHeaders.set("anthropic-beta", mergeBetas(requestHeaders.get("anthropic-beta"), betas));
777
+ requestHeaders.set("anthropic-dangerous-direct-browser-access", "true");
778
+ requestHeaders.set("authorization", `Bearer ${auth.access}`);
779
+ requestHeaders.set("user-agent", process.env.OPENCODE_ANTHROPIC_USER_AGENT ||
780
+ `claude-cli/${CLAUDE_CODE_VERSION}`);
781
+ requestHeaders.set("x-app", "cli");
782
+ requestHeaders.delete("x-api-key");
750
783
  return fetch(input, {
751
784
  ...(init ?? {}),
752
785
  body: rewritten.body,
@@ -761,20 +794,22 @@ const AnthropicAuthPlugin = async ({ client }) => {
761
794
  const bodyText = await response
762
795
  .clone()
763
796
  .text()
764
- .catch(() => '');
797
+ .catch(() => "");
765
798
  if (shouldRotateAuth(response.status, bodyText)) {
766
799
  const rotated = await rotateAnthropicAccount(freshAuth, client);
767
800
  if (rotated) {
768
801
  // Show toast notification so Discord thread shows the rotation
769
- client.tui.showToast({
802
+ client.tui
803
+ .showToast({
770
804
  body: {
771
805
  message: appendToastSessionMarker({
772
806
  message: `Switching from account ${rotated.fromLabel} to account ${rotated.toLabel}`,
773
807
  sessionId,
774
808
  }),
775
- variant: 'info',
809
+ variant: "info",
776
810
  },
777
- }).catch(() => { });
811
+ })
812
+ .catch(() => { });
778
813
  const retryAuth = await getFreshOAuth(getAuth, client);
779
814
  if (retryAuth) {
780
815
  response = await runRequest(retryAuth);
@@ -788,22 +823,36 @@ const AnthropicAuthPlugin = async ({ client }) => {
788
823
  },
789
824
  methods: [
790
825
  {
791
- label: 'Claude Pro/Max',
792
- type: 'oauth',
793
- authorize: buildAuthorizeHandler('oauth'),
826
+ label: "Claude Pro/Max",
827
+ type: "oauth",
828
+ authorize: buildAuthorizeHandler("oauth"),
794
829
  },
795
830
  {
796
- label: 'Create an API Key',
797
- type: 'oauth',
798
- authorize: buildAuthorizeHandler('apikey'),
831
+ label: "Create an API Key",
832
+ type: "oauth",
833
+ authorize: buildAuthorizeHandler("apikey"),
799
834
  },
800
835
  {
801
- provider: 'anthropic',
802
- label: 'Manually enter API Key',
803
- type: 'api',
836
+ provider: "anthropic",
837
+ label: "Manually enter API Key",
838
+ type: "api",
804
839
  },
805
840
  ],
806
841
  },
807
842
  };
808
843
  };
809
- export { AnthropicAuthPlugin as anthropicAuthPlugin, };
844
+ const replacer = async () => {
845
+ return {
846
+ "experimental.chat.system.transform": (async (input, output) => {
847
+ if (input.model.providerID !== "anthropic")
848
+ return;
849
+ const textIndex = output.system.findIndex((x) => x.includes(OPENCODE_IDENTITY));
850
+ const text = output.system[textIndex];
851
+ if (!text) {
852
+ return;
853
+ }
854
+ output.system[textIndex] = sanitizeAnthropicSystemText(text);
855
+ }),
856
+ };
857
+ };
858
+ export { replacer, AnthropicAuthPlugin as anthropicAuthPlugin };