@xtruder/opencode-claude-max-plugin 0.1.11 → 0.1.12

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.md CHANGED
@@ -10,58 +10,42 @@ An [OpenCode](https://opencode.ai/) plugin that enables Claude Pro/Max subscript
10
10
  - **All Claude models** — Opus 4.6, Sonnet 4.6, Haiku 4.5
11
11
  - **Extended thinking** — Full reasoning support with thinking variants (high/max)
12
12
  - **Usage tracking** — Built-in `/usage` command shows subscription utilization
13
+ - **Self-registering** — Models are registered automatically, no manual provider config needed
13
14
 
14
15
  ## Installation
15
16
 
16
- You do not need to install the package manually — OpenCode auto-installs it when it first loads your config.
17
+ Add the plugin to your `opencode.json` (project-level or `~/.config/opencode/opencode.json` globally):
17
18
 
18
- Just add the following to `.opencode/opencode.json` in your project (or `~/.config/opencode/opencode.json` globally):
19
+ ```json
20
+ {
21
+ "$schema": "https://opencode.ai/config.json",
22
+ "plugin": ["@xtruder/opencode-claude-max-plugin"]
23
+ }
24
+ ```
25
+
26
+ That's it. The plugin self-registers the `anthropic-sdk` provider and its models (Haiku 4.5, Sonnet 4.6, Opus 4.6) at startup via the OpenCode config hook. No separate `provider` block is needed.
27
+
28
+ Then open OpenCode and run `/connect` → Other → `anthropic-sdk`. If Claude Code is installed and you're logged in, credentials are read automatically from `~/.claude/.credentials.json` — no API key needed.
29
+
30
+ ### Custom model options
31
+
32
+ If you want to override model settings (e.g. thinking budgets, variants), you can add a `provider` block alongside the plugin:
19
33
 
20
34
  ```json
21
35
  {
22
36
  "$schema": "https://opencode.ai/config.json",
37
+ "plugin": ["@xtruder/opencode-claude-max-plugin"],
23
38
  "provider": {
24
39
  "anthropic-sdk": {
25
- "npm": "@xtruder/opencode-claude-max-plugin",
26
- "name": "Anthropic SDK",
27
40
  "models": {
28
41
  "claude-sonnet-4-6": {
29
- "name": "Claude Sonnet 4.6",
30
- "attachment": false,
31
- "reasoning": true,
32
- "tool_call": true,
33
- "temperature": true,
34
- "limit": { "context": 200000, "output": 64000 },
35
42
  "options": {
36
43
  "thinking": { "type": "enabled", "budgetTokens": 1024 }
37
44
  },
38
45
  "variants": {
39
46
  "high": { "thinking": { "type": "enabled", "budgetTokens": 10000 } },
40
- "max": { "thinking": { "type": "enabled", "budgetTokens": 32000 } }
41
- }
42
- },
43
- "claude-opus-4-6": {
44
- "name": "Claude Opus 4.6",
45
- "attachment": false,
46
- "reasoning": true,
47
- "tool_call": true,
48
- "temperature": true,
49
- "limit": { "context": 1000000, "output": 64000 },
50
- "options": {
51
- "thinking": { "type": "adaptive" }
52
- },
53
- "variants": {
54
- "high": { "thinking": { "type": "enabled", "budgetTokens": 10000 } },
55
- "max": { "thinking": { "type": "enabled", "budgetTokens": 32000 } }
47
+ "max": { "thinking": { "type": "enabled", "budgetTokens": 32000 } }
56
48
  }
57
- },
58
- "claude-haiku-4-5-20251001": {
59
- "name": "Claude Haiku 4.5",
60
- "attachment": false,
61
- "reasoning": false,
62
- "tool_call": true,
63
- "temperature": true,
64
- "limit": { "context": 200000, "output": 8192 }
65
49
  }
66
50
  }
67
51
  }
@@ -69,7 +53,7 @@ Just add the following to `.opencode/opencode.json` in your project (or `~/.conf
69
53
  }
70
54
  ```
71
55
 
72
- Then open OpenCode and run `/connect` → Other → `anthropic-sdk`. If Claude Code is installed and you're logged in, credentials are read automatically from `~/.claude/.credentials.json` — no API key needed.
56
+ Config-level settings are merged with plugin defaults — you only need to specify what you want to override.
73
57
 
74
58
  ## Authentication
75
59
 
@@ -112,16 +96,16 @@ After adding the config, run `/usage` inside OpenCode to see your subscription u
112
96
  The plugin also works as a standalone Vercel AI SDK provider:
113
97
 
114
98
  ```typescript
115
- import { createAnthropicSDK } from "@xtruder/opencode-claude-max-plugin";
116
- import { streamText } from "ai";
99
+ import { createAnthropicSDK } from "@xtruder/opencode-claude-max-plugin"
100
+ import { streamText } from "ai"
117
101
 
118
102
  // Uses ~/.claude/.credentials.json automatically
119
- const provider = createAnthropicSDK();
120
- const model = provider.languageModel("claude-sonnet-4-6");
103
+ const provider = createAnthropicSDK()
104
+ const model = provider.languageModel("claude-sonnet-4-6")
121
105
 
122
- const result = streamText({ model, prompt: "Hello!" });
106
+ const result = streamText({ model, prompt: "Hello!" })
123
107
  for await (const chunk of result.textStream) {
124
- process.stdout.write(chunk);
108
+ process.stdout.write(chunk)
125
109
  }
126
110
  ```
127
111
 
@@ -130,7 +114,7 @@ for await (const chunk of result.textStream) {
130
114
  ```bash
131
115
  bun install
132
116
  bun run build
133
- bun run test # 17 integration tests (requires ANTHROPIC_API_KEY or Claude Code credentials)
117
+ bun run test # integration tests (requires ANTHROPIC_API_KEY or Claude Code credentials)
134
118
  ```
135
119
 
136
120
  See [RESEARCH.md](RESEARCH.md) for detailed reverse-engineering findings on how we matched Claude Code's request format.
@@ -15,7 +15,30 @@ export declare function getCredentialsPath(): string;
15
15
  */
16
16
  export declare function readClaudeCredentials(path?: string): ClaudeOAuthCredentials | null;
17
17
  /**
18
- * Check if credentials are expired (with 5 minute buffer).
18
+ * Check if credentials are expired (with 60s buffer).
19
19
  */
20
20
  export declare function isExpired(creds: ClaudeOAuthCredentials): boolean;
21
+ /**
22
+ * Run the Claude CLI to trigger an OAuth token refresh.
23
+ * The CLI writes refreshed credentials to ~/.claude/.credentials.json.
24
+ * Retries once on failure (matching opencode-claude-auth behavior).
25
+ */
26
+ export declare function refreshViaCli(): boolean;
27
+ /**
28
+ * Read credentials, refreshing via Claude CLI if the token is near expiry.
29
+ * Returns null only if credentials are completely unavailable.
30
+ */
31
+ export declare function refreshIfNeeded(path?: string): ClaudeOAuthCredentials | null;
32
+ /**
33
+ * Get cached credentials with automatic refresh.
34
+ * Uses in-memory caching with a 30-second TTL to avoid
35
+ * hitting the filesystem on every request.
36
+ * When the cached token is near expiry, triggers a CLI refresh.
37
+ */
38
+ export declare function getCachedCredentials(path?: string): ClaudeOAuthCredentials | null;
39
+ /**
40
+ * Clear the in-memory credential cache.
41
+ * Forces the next getCachedCredentials() call to re-read from disk.
42
+ */
43
+ export declare function clearCredentialCache(): void;
21
44
  //# sourceMappingURL=credentials.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;CAChC;AAMD;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,CAAC,EAAE,MAAM,GACZ,sBAAsB,GAAG,IAAI,CAa/B;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAGhE"}
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;CAChC;AAqBD;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI,CAalF;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAEhE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAgBvC;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI,CAyB5E;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI,CAoBjF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAI3C"}
package/build/index.d.ts CHANGED
@@ -1,19 +1,5 @@
1
1
  import type { LanguageModelV2 } from "@ai-sdk/provider";
2
- /**
3
- * Beta flags that Claude Code sends on every OAuth request.
4
- * Order and exact values must match what Claude Code sends.
5
- */
6
- export declare const OAUTH_BETAS: string[];
7
- /**
8
- * Beta flag that enables 1M context window. Only sent when model ID
9
- * includes [1m] suffix — e.g. "claude-sonnet-4-6[1m]".
10
- * Claude Code checks: /\[1m\]/i.test(modelId) before including this.
11
- */
12
- export declare const CONTEXT_1M_BETA = "context-1m-2025-08-07";
13
- /**
14
- * Beta flags for regular API key auth (no OAuth).
15
- */
16
- export declare const API_KEY_BETAS: string[];
2
+ import type { Plugin } from "@opencode-ai/plugin";
17
3
  export interface AnthropicSDKProviderOptions {
18
4
  /**
19
5
  * Anthropic API key. Defaults to ANTHROPIC_API_KEY env var.
@@ -51,30 +37,34 @@ export interface AnthropicSDKProvider {
51
37
  */
52
38
  languageModel(modelId: string): LanguageModelV2;
53
39
  }
54
- /**
55
- * Cached ratelimit usage from the most recent API response headers.
56
- * Updated on every successful inference call — same data Claude Code
57
- * reads for its /usage display. No extra API call needed.
58
- */
59
- export interface CachedUsage {
60
- fiveHourUtil?: number;
61
- sevenDayUtil?: number;
62
- fiveHourReset?: number;
63
- sevenDayReset?: number;
64
- overageStatus?: string;
65
- timestamp?: number;
66
- }
67
- export declare const cachedUsage: CachedUsage;
68
40
  /**
69
41
  * Create an Anthropic SDK provider for use with OpenCode / Vercel AI SDK.
70
42
  *
71
43
  * When using OAuth credentials from Claude Code, requests are made to
72
44
  * look identical to Claude Code CLI requests — same headers, user-agent,
73
45
  * beta flags, billing system block, and body structure.
46
+ *
47
+ * OpenCode discovers this function by looking for an export whose name
48
+ * starts with "create" when loading the npm package as a provider.
74
49
  */
75
50
  export declare function createAnthropicSDK(options?: AnthropicSDKProviderOptions): AnthropicSDKProvider;
51
+ /**
52
+ * OpenCode plugin that self-registers the anthropic-sdk provider and its
53
+ * supported models into the running config on startup.
54
+ *
55
+ * Add this package to the `plugin` array in your opencode.json:
56
+ *
57
+ * { "plugin": ["@xtruder/opencode-claude-max-plugin"] }
58
+ *
59
+ * The plugin uses the `config` hook which is called after config is loaded
60
+ * but before providers are initialized. We mutate the Config object in-place
61
+ * to inject the provider definition and model list.
62
+ *
63
+ * OpenCode's plugin loader iterates all module exports and calls each one
64
+ * as a Plugin function. `createAnthropicSDK` is also called — it returns
65
+ * { languageModel } which is harmlessly pushed to the hooks list with all
66
+ * hook slots undefined (no-ops).
67
+ */
68
+ export declare const anthropicSDKPlugin: Plugin;
76
69
  export default createAnthropicSDK;
77
- export { AnthropicSDKModel } from "./model.js";
78
- export type { ConvertedPrompt } from "./prompt.js";
79
- export { readClaudeCredentials, isExpired, getCredentialsPath, type ClaudeOAuthCredentials, } from "./credentials.js";
80
70
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAQvD;;;GAGG;AACH,eAAO,MAAM,WAAW,UAOvB,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,0BAA0B,CAAA;AAEtD;;GAEG;AACH,eAAO,MAAM,aAAa,UAGzB,CAAA;AAED,MAAM,WAAW,2BAA2B;IAC1C;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAEhC;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;IAE/B;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IAEb;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IAExB;;OAEG;IACH,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,CAAA;CAChD;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,eAAO,MAAM,WAAW,EAAE,WAAgB,CAAA;AAgC1C;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,GAAE,2BAAgC,GACxC,oBAAoB,CA0GtB;AAGD,eAAe,kBAAkB,CAAA;AAGjC,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC9C,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,EACL,qBAAqB,EACrB,SAAS,EACT,kBAAkB,EAClB,KAAK,sBAAsB,GAC5B,MAAM,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAoFjD,MAAM,WAAW,2BAA2B;IAC1C;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAEhC;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;IAE/B;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IAEb;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IAExB;;OAEG;IACH,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,CAAA;CAChD;AA4BD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,GAAE,2BAAgC,GACxC,oBAAoB,CAiHtB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAoBhC,CAAA;AAID,eAAe,kBAAkB,CAAA"}
package/build/index.js CHANGED
@@ -28,7 +28,6 @@ var BUILTIN_OPENCODE_TO_CLAUDE = {
28
28
  codesearch: "CodeSearch",
29
29
  lsp: "LSP"
30
30
  };
31
- var BUILTIN_OPENCODE_NAMES = new Set(Object.keys(BUILTIN_OPENCODE_TO_CLAUDE));
32
31
  var BUILTIN_CLAUDE_TO_OPENCODE = {};
33
32
  for (const [oc, cc] of Object.entries(BUILTIN_OPENCODE_TO_CLAUDE)) {
34
33
  BUILTIN_CLAUDE_TO_OPENCODE[cc] = oc;
@@ -55,7 +54,7 @@ function detectMcpServers() {
55
54
  }
56
55
  } catch {}
57
56
  }
58
- return [...servers].sort((a, b) => b.length - a.length);
57
+ return [...servers].toSorted((a, b) => b.length - a.length);
59
58
  }
60
59
  function getMcpServers() {
61
60
  if (mcpServerNames === null) {
@@ -265,10 +264,19 @@ function formatToolResultContent(output) {
265
264
  }
266
265
  }
267
266
 
267
+ // src/usage-cache.ts
268
+ var cachedUsage = {};
269
+
268
270
  // src/credentials.ts
269
271
  import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
272
+ import { execSync } from "node:child_process";
270
273
  import { join as join2 } from "node:path";
271
274
  import { homedir as homedir2 } from "node:os";
275
+ var CREDENTIAL_CACHE_TTL_MS = 30000;
276
+ var EXPIRY_BUFFER_MS = 60000;
277
+ var cachedCreds = null;
278
+ var cachedAt = 0;
279
+ var cachedPath;
272
280
  function getCredentialsPath() {
273
281
  return join2(homedir2(), ".claude", ".credentials.json");
274
282
  }
@@ -287,8 +295,57 @@ function readClaudeCredentials(path) {
287
295
  }
288
296
  }
289
297
  function isExpired(creds) {
290
- const buffer = 5 * 60 * 1000;
291
- return Date.now() > creds.expiresAt - buffer;
298
+ return Date.now() > creds.expiresAt - EXPIRY_BUFFER_MS;
299
+ }
300
+ function refreshViaCli() {
301
+ const maxAttempts = 2;
302
+ for (let i = 0;i < maxAttempts; i++) {
303
+ try {
304
+ execSync("claude -p . --model haiku", {
305
+ timeout: 60000,
306
+ encoding: "utf-8",
307
+ env: { ...process.env, TERM: "dumb" },
308
+ stdio: "ignore"
309
+ });
310
+ return true;
311
+ } catch {}
312
+ }
313
+ return false;
314
+ }
315
+ function refreshIfNeeded(path) {
316
+ const creds = readClaudeCredentials(path);
317
+ if (!creds)
318
+ return null;
319
+ if (!isExpired(creds))
320
+ return creds;
321
+ console.warn("[anthropic-sdk-provider] OAuth token near expiry, refreshing via Claude CLI...");
322
+ const refreshed = refreshViaCli();
323
+ if (!refreshed) {
324
+ console.warn("[anthropic-sdk-provider] CLI refresh failed. Token may be expired.");
325
+ return creds;
326
+ }
327
+ const fresh = readClaudeCredentials(path);
328
+ if (fresh && !isExpired(fresh)) {
329
+ console.warn("[anthropic-sdk-provider] Token refreshed successfully.");
330
+ return fresh;
331
+ }
332
+ return fresh ?? creds;
333
+ }
334
+ function getCachedCredentials(path) {
335
+ const now = Date.now();
336
+ const resolvedPath = path ?? cachedPath;
337
+ if (cachedCreds && now - cachedAt < CREDENTIAL_CACHE_TTL_MS && !isExpired(cachedCreds)) {
338
+ return cachedCreds;
339
+ }
340
+ const fresh = refreshIfNeeded(resolvedPath);
341
+ if (fresh) {
342
+ cachedCreds = fresh;
343
+ cachedAt = now;
344
+ cachedPath = resolvedPath;
345
+ } else {
346
+ cachedCreds = null;
347
+ }
348
+ return cachedCreds;
292
349
  }
293
350
 
294
351
  // src/usage.ts
@@ -682,8 +739,8 @@ function formatReset2(epoch) {
682
739
  const dateStr = reset.toLocaleDateString("en-US", { month: "short", day: "numeric" });
683
740
  return diffMs < 86400000 ? `Resets ${timeStr}` : `Resets ${dateStr}, ${timeStr}`;
684
741
  }
742
+ var bar = (pct, width = 50) => "█".repeat(Math.round(pct / 100 * width)) + " ".repeat(width - Math.round(pct / 100 * width));
685
743
  function formatCachedUsage(usage) {
686
- const bar = (pct, width = 50) => "█".repeat(Math.round(pct / 100 * width)) + " ".repeat(width - Math.round(pct / 100 * width));
687
744
  const lines = [];
688
745
  const h5 = Math.round((usage.fiveHourUtil ?? 0) * 100);
689
746
  lines.push(" Current session");
@@ -763,7 +820,11 @@ class AnthropicSDKModel {
763
820
  const rewrite = (s) => rewriteToolNamesInText(s);
764
821
  const SYSTEM_CACHE = { type: "ephemeral", ttl: "1h", scope: "global" };
765
822
  if (system) {
766
- const contentBlocks = typeof system === "string" ? [{ type: "text", text: rewrite(system), cache_control: SYSTEM_CACHE }] : Array.isArray(system) ? system.map((b, i) => b.type === "text" ? { ...b, text: rewrite(b.text), ...i === system.length - 1 ? { cache_control: SYSTEM_CACHE } : {} } : b) : [{ ...system, cache_control: SYSTEM_CACHE }];
823
+ const contentBlocks = typeof system === "string" ? [{ type: "text", text: rewrite(system), cache_control: SYSTEM_CACHE }] : Array.isArray(system) ? system.map((b, i) => b.type === "text" ? {
824
+ ...b,
825
+ text: rewrite(b.text),
826
+ ...i === system.length - 1 ? { cache_control: SYSTEM_CACHE } : {}
827
+ } : b) : [{ ...system, cache_control: SYSTEM_CACHE }];
767
828
  params.system = [BILLING_SYSTEM_BLOCK, IDENTITY_SYSTEM_BLOCK, ...contentBlocks];
768
829
  } else {
769
830
  params.system = [BILLING_SYSTEM_BLOCK, IDENTITY_SYSTEM_BLOCK];
@@ -787,7 +848,10 @@ class AnthropicSDKModel {
787
848
  const blocks = system.map((b, i) => b.type === "text" && i === system.length - 1 ? { ...b, cache_control: CACHE } : b);
788
849
  params.system = [{ ...IDENTITY_SYSTEM_BLOCK, cache_control: CACHE }, ...blocks];
789
850
  } else {
790
- params.system = [{ ...IDENTITY_SYSTEM_BLOCK, cache_control: CACHE }, { ...system, cache_control: CACHE }];
851
+ params.system = [
852
+ { ...IDENTITY_SYSTEM_BLOCK, cache_control: CACHE },
853
+ { ...system, cache_control: CACHE }
854
+ ];
791
855
  }
792
856
  }
793
857
  if (tools && tools.length > 0)
@@ -973,11 +1037,50 @@ var OAUTH_BETAS = [
973
1037
  "effort-2025-11-24"
974
1038
  ];
975
1039
  var CONTEXT_1M_BETA = "context-1m-2025-08-07";
976
- var API_KEY_BETAS = [
977
- "interleaved-thinking-2025-05-14",
978
- "fine-grained-tool-streaming-2025-05-14"
979
- ];
980
- var cachedUsage = {};
1040
+ var API_KEY_BETAS = ["interleaved-thinking-2025-05-14", "fine-grained-tool-streaming-2025-05-14"];
1041
+ var PACKAGE_NAME = "@xtruder/opencode-claude-max-plugin";
1042
+ var PROVIDER_ID = "anthropic-sdk";
1043
+ var PLUGIN_MODELS = {
1044
+ "claude-haiku-4-5": {
1045
+ name: "Claude Haiku 4.5",
1046
+ reasoning: false,
1047
+ tool_call: true,
1048
+ attachment: true,
1049
+ temperature: true,
1050
+ limit: { context: 200000, output: 8192 },
1051
+ cost: { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 },
1052
+ modalities: {
1053
+ input: ["text", "image"],
1054
+ output: ["text"]
1055
+ }
1056
+ },
1057
+ "claude-sonnet-4-6": {
1058
+ name: "Claude Sonnet 4.6",
1059
+ reasoning: true,
1060
+ tool_call: true,
1061
+ attachment: true,
1062
+ temperature: true,
1063
+ limit: { context: 200000, output: 16000 },
1064
+ cost: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
1065
+ modalities: {
1066
+ input: ["text", "image", "pdf"],
1067
+ output: ["text"]
1068
+ }
1069
+ },
1070
+ "claude-opus-4-6": {
1071
+ name: "Claude Opus 4.6",
1072
+ reasoning: true,
1073
+ tool_call: true,
1074
+ attachment: true,
1075
+ temperature: true,
1076
+ limit: { context: 200000, output: 32000 },
1077
+ cost: { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 },
1078
+ modalities: {
1079
+ input: ["text", "image", "pdf"],
1080
+ output: ["text"]
1081
+ }
1082
+ }
1083
+ };
981
1084
  function resolveAuth(options) {
982
1085
  if (options.apiKey) {
983
1086
  return { apiKey: options.apiKey, isOAuth: false };
@@ -985,25 +1088,16 @@ function resolveAuth(options) {
985
1088
  if (process.env.ANTHROPIC_API_KEY) {
986
1089
  return { apiKey: process.env.ANTHROPIC_API_KEY, isOAuth: false };
987
1090
  }
988
- const creds = readClaudeCredentials(options.credentialsPath);
1091
+ const credentialsPath = typeof options.credentialsPath === "string" ? options.credentialsPath : undefined;
1092
+ const creds = getCachedCredentials(credentialsPath);
989
1093
  if (creds) {
990
- if (isExpired(creds)) {
991
- console.warn("[anthropic-sdk-provider] Claude Code OAuth token is expired. " + "Run 'claude' to re-authenticate, then restart.");
992
- }
993
1094
  return { apiKey: null, authToken: creds.accessToken, isOAuth: true };
994
1095
  }
995
1096
  return { isOAuth: false };
996
1097
  }
997
1098
  function createAnthropicSDK(options = {}) {
998
- const {
999
- apiKey,
1000
- baseURL,
1001
- headers,
1002
- fetch: customFetch,
1003
- name = "anthropic-sdk",
1004
- credentialsPath,
1005
- ...rest
1006
- } = options;
1099
+ const { baseURL, headers, fetch: customFetch, name = "anthropic-sdk" } = options;
1100
+ const credentialsPath = typeof options.credentialsPath === "string" ? options.credentialsPath : undefined;
1007
1101
  const auth = resolveAuth(options);
1008
1102
  const defaultHeaders = {
1009
1103
  ...headers,
@@ -1018,12 +1112,20 @@ function createAnthropicSDK(options = {}) {
1018
1112
  const CONTEXT_1M_BODY_THRESHOLD = 600000;
1019
1113
  const baseFetch = customFetch ?? globalThis.fetch;
1020
1114
  const wrappedFetch = async (url, init) => {
1115
+ if (auth.isOAuth && init) {
1116
+ const freshCreds = getCachedCredentials(credentialsPath);
1117
+ if (freshCreds) {
1118
+ const reqHeaders = new Headers(init.headers);
1119
+ reqHeaders.set("authorization", `Bearer ${freshCreds.accessToken}`);
1120
+ init = { ...init, headers: reqHeaders };
1121
+ }
1122
+ }
1021
1123
  if (init?.body && typeof init.body === "string" && init.body.length > CONTEXT_1M_BODY_THRESHOLD) {
1022
- const headers2 = new Headers(init.headers);
1023
- const existingBetas = headers2.get("anthropic-beta") ?? "";
1124
+ const betaHeaders = new Headers(init.headers);
1125
+ const existingBetas = betaHeaders.get("anthropic-beta") ?? "";
1024
1126
  if (!existingBetas.includes(CONTEXT_1M_BETA)) {
1025
- headers2.set("anthropic-beta", existingBetas + "," + CONTEXT_1M_BETA);
1026
- init = { ...init, headers: headers2 };
1127
+ betaHeaders.set("anthropic-beta", existingBetas + "," + CONTEXT_1M_BETA);
1128
+ init = { ...init, headers: betaHeaders };
1027
1129
  }
1028
1130
  }
1029
1131
  const resp = await baseFetch(url, init);
@@ -1043,13 +1145,12 @@ function createAnthropicSDK(options = {}) {
1043
1145
  const isLongContextLimit = bodyText.includes("Extra usage is required for long context");
1044
1146
  const isSubscriptionLimit = isLongContextLimit || unifiedStatus === "over_limit" || retryAfter > 120;
1045
1147
  if (isSubscriptionLimit) {
1046
- const body = bodyText;
1047
- const headers2 = new Headers(resp.headers);
1048
- headers2.set("x-should-retry", "false");
1049
- return new Response(body, {
1148
+ const retryHeaders = new Headers(resp.headers);
1149
+ retryHeaders.set("x-should-retry", "false");
1150
+ return new Response(bodyText, {
1050
1151
  status: resp.status,
1051
1152
  statusText: resp.statusText,
1052
- headers: headers2
1153
+ headers: retryHeaders
1053
1154
  });
1054
1155
  }
1055
1156
  }
@@ -1068,16 +1169,23 @@ function createAnthropicSDK(options = {}) {
1068
1169
  }
1069
1170
  };
1070
1171
  }
1172
+ var anthropicSDKPlugin = async () => {
1173
+ return {
1174
+ config: async (cfg) => {
1175
+ if (!cfg.provider) {
1176
+ cfg.provider = {};
1177
+ }
1178
+ cfg.provider[PROVIDER_ID] = {
1179
+ npm: PACKAGE_NAME,
1180
+ models: PLUGIN_MODELS,
1181
+ ...cfg.provider[PROVIDER_ID]
1182
+ };
1183
+ }
1184
+ };
1185
+ };
1071
1186
  var src_default = createAnthropicSDK;
1072
1187
  export {
1073
- readClaudeCredentials,
1074
- isExpired,
1075
- getCredentialsPath,
1076
1188
  src_default as default,
1077
1189
  createAnthropicSDK,
1078
- cachedUsage,
1079
- OAUTH_BETAS,
1080
- CONTEXT_1M_BETA,
1081
- AnthropicSDKModel,
1082
- API_KEY_BETAS
1190
+ anthropicSDKPlugin
1083
1191
  };
@@ -1 +1 @@
1
- {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,0BAA0B,EAK3B,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,SAAS,MAAM,mBAAmB,CAAA;AAU9C,KAAK,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;AAC1E,KAAK,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;AAmMtE,qBAAa,iBAAkB,YAAW,eAAe;IAQrD,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,OAAO;IATjB,QAAQ,CAAC,oBAAoB,EAAG,IAAI,CAAS;IAC7C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAK;gBAGnD,OAAO,EAAE,MAAM,EACP,MAAM,EAAE,SAAS,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,OAAe;IAMlC,OAAO,CAAC,WAAW;IAsLb,UAAU,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA+E1E,QAAQ,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,cAAc,CAAC;CA4C7E"}
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,0BAA0B,EAK3B,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,SAAS,MAAM,mBAAmB,CAAA;AAU9C,KAAK,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;AAC1E,KAAK,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;AAsLtE,qBAAa,iBAAkB,YAAW,eAAe;IAQrD,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,OAAO;IATjB,QAAQ,CAAC,oBAAoB,EAAG,IAAI,CAAS;IAC7C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAK;gBAGnD,OAAO,EAAE,MAAM,EACP,MAAM,EAAE,SAAS,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,OAAe;IAMlC,OAAO,CAAC,WAAW;IAqLb,UAAU,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAoF1E,QAAQ,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,cAAc,CAAC;CA6C7E"}
@@ -1 +1 @@
1
- {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EAE1B,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,SAAS,MAAM,mBAAmB,CAAA;AAG9C,KAAK,kBAAkB,GAAG,SAAS,CAAC,kBAAkB,CAAA;AAiCtD,wBAAgB,aAAa,CAC3B,eAAe,EAAE,aAAa,CAAC,kBAAkB,CAAC,EAClD,OAAO,EAAE,MAAM,GACd,cAAc,CAAC,yBAAyB,CAAC,CAiD3C"}
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAA+B,MAAM,kBAAkB,CAAA;AAC9F,OAAO,KAAK,SAAS,MAAM,mBAAmB,CAAA;AAG9C,KAAK,kBAAkB,GAAG,SAAS,CAAC,kBAAkB,CAAA;AAiCtD,wBAAgB,aAAa,CAC3B,eAAe,EAAE,aAAa,CAAC,kBAAkB,CAAC,EAClD,OAAO,EAAE,MAAM,GACd,cAAc,CAAC,yBAAyB,CAAC,CAqD3C"}
@@ -1 +1 @@
1
- {"version":3,"file":"tool-names.d.ts","sourceRoot":"","sources":["../src/tool-names.ts"],"names":[],"mappings":"AAoDA;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CAE1D;AAgED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAY7D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAiB7D;AAmCD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM3D"}
1
+ {"version":3,"file":"tool-names.d.ts","sourceRoot":"","sources":["../src/tool-names.ts"],"names":[],"mappings":"AAiDA;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CAE1D;AAgED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAY7D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAiB7D;AAqCD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM3D"}
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,2BAA2B,EAC3B,kCAAkC,EAClC,yBAAyB,EAC1B,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,SAAS,MAAM,mBAAmB,CAAA;AAG9C,KAAK,aAAa,GAAG,SAAS,CAAC,IAAI,CAAA;AACnC,KAAK,mBAAmB,GAAG,SAAS,CAAC,mBAAmB,CAAC,aAAa,CAAC,CAAA;AAwBvE,wBAAgB,YAAY,CAC1B,KAAK,EAAE,KAAK,CAAC,2BAA2B,GAAG,kCAAkC,CAAC,GAAG,SAAS,GACzF,aAAa,EAAE,GAAG,SAAS,CAU7B;AAED,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,yBAAyB,GAAG,SAAS,GAChD,mBAAmB,GAAG,SAAS,CAcjC"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,2BAA2B,EAC3B,kCAAkC,EAClC,yBAAyB,EAC1B,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,SAAS,MAAM,mBAAmB,CAAA;AAG9C,KAAK,aAAa,GAAG,SAAS,CAAC,IAAI,CAAA;AACnC,KAAK,mBAAmB,GAAG,SAAS,CAAC,mBAAmB,CAAC,aAAa,CAAC,CAAA;AAwBvE,wBAAgB,YAAY,CAC1B,KAAK,EAAE,KAAK,CAAC,2BAA2B,GAAG,kCAAkC,CAAC,GAAG,SAAS,GACzF,aAAa,EAAE,GAAG,SAAS,CAY7B;AAED,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,yBAAyB,GAAG,SAAS,GAChD,mBAAmB,GAAG,SAAS,CAcjC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Cached ratelimit usage from the most recent API response headers.
3
+ * Updated on every successful inference call — same data Claude Code
4
+ * reads for its /usage display. No extra API call needed.
5
+ */
6
+ export interface CachedUsage {
7
+ fiveHourUtil?: number;
8
+ sevenDayUtil?: number;
9
+ fiveHourReset?: number;
10
+ sevenDayReset?: number;
11
+ overageStatus?: string;
12
+ timestamp?: number;
13
+ }
14
+ export declare const cachedUsage: CachedUsage;
15
+ //# sourceMappingURL=usage-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage-cache.d.ts","sourceRoot":"","sources":["../src/usage-cache.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,eAAO,MAAM,WAAW,EAAE,WAAgB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,WAAW,GAAG,IAAI,CAAA;IAC7B,SAAS,EAAE,WAAW,GAAG,IAAI,CAAA;IAC7B,gBAAgB,EAAE,WAAW,GAAG,IAAI,CAAA;IACpC,cAAc,EAAE,WAAW,GAAG,IAAI,CAAA;IAClC,oBAAoB,EAAE,WAAW,GAAG,IAAI,CAAA;IACxC,gBAAgB,EAAE,WAAW,GAAG,IAAI,CAAA;IACpC,WAAW,EAAE;QACX,UAAU,EAAE,OAAO,CAAA;QACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;QAC5B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;QAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;KAC3B,GAAG,IAAI,CAAA;CACT;AAID;;;GAGG;AACH,wBAAsB,UAAU,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAuBpF;AA0CD;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAsDnD"}
1
+ {"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,WAAW,GAAG,IAAI,CAAA;IAC7B,SAAS,EAAE,WAAW,GAAG,IAAI,CAAA;IAC7B,gBAAgB,EAAE,WAAW,GAAG,IAAI,CAAA;IACpC,cAAc,EAAE,WAAW,GAAG,IAAI,CAAA;IAClC,oBAAoB,EAAE,WAAW,GAAG,IAAI,CAAA;IACxC,gBAAgB,EAAE,WAAW,GAAG,IAAI,CAAA;IACpC,WAAW,EAAE;QACX,UAAU,EAAE,OAAO,CAAA;QACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;QAC5B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;QAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;KAC3B,GAAG,IAAI,CAAA;CACT;AAID;;;GAGG;AACH,wBAAsB,UAAU,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAuBpF;AA0CD;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAwDnD"}
package/package.json CHANGED
@@ -1,7 +1,18 @@
1
1
  {
2
2
  "name": "@xtruder/opencode-claude-max-plugin",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "OpenCode plugin for Claude Max/Pro subscription via @anthropic-ai/sdk - uses Claude Code OAuth credentials",
5
+ "keywords": [
6
+ "ai-sdk",
7
+ "anthropic",
8
+ "claude",
9
+ "opencode",
10
+ "provider"
11
+ ],
12
+ "license": "MIT",
13
+ "files": [
14
+ "build"
15
+ ],
5
16
  "type": "module",
6
17
  "main": "build/index.js",
7
18
  "types": "build/index.d.ts",
@@ -9,42 +20,37 @@
9
20
  ".": {
10
21
  "import": "./build/index.js",
11
22
  "types": "./build/index.d.ts"
12
- },
13
- "./agent": {
14
- "import": "./build/agent-index.js",
15
- "types": "./build/agent-index.d.ts"
16
23
  }
17
24
  },
18
- "bin": {
19
- "claude-usage": "build/usage-cli.js"
20
- },
21
- "files": [
22
- "build"
23
- ],
24
25
  "scripts": {
25
- "build": "bun build ./src/index.ts ./src/agent-index.ts ./src/usage-cli.ts --outdir ./build --target node --format esm --packages external && bun x tsc --emitDeclarationOnly --declaration --outDir build",
26
- "test": "bun run src/test.ts"
26
+ "build": "bun build ./src/index.ts --outdir ./build --target node --format esm --packages external && bun x tsc --emitDeclarationOnly --declaration --outDir build",
27
+ "dev": "bun run dev:config && bun build ./src/index.ts --outdir ./build --target node --format esm --packages external --watch",
28
+ "dev:config": "bun scripts/dev-config.ts",
29
+ "test": "bun test --concurrency 1 src/*.test.ts",
30
+ "lint": "oxlint src",
31
+ "lint:fix": "oxlint --fix src",
32
+ "format": "oxfmt --write .",
33
+ "format:check": "oxfmt --check .",
34
+ "prepare": "simple-git-hooks"
27
35
  },
28
36
  "dependencies": {
29
- "@anthropic-ai/claude-agent-sdk": "^0.2.81",
30
37
  "@anthropic-ai/sdk": "0.74.0"
31
38
  },
32
- "peerDependencies": {
33
- "@ai-sdk/provider": ">=2.0.0"
34
- },
35
39
  "devDependencies": {
36
40
  "@ai-sdk/provider": "2.0.1",
37
41
  "@ai-sdk/provider-utils": "3.0.2",
42
+ "@opencode-ai/plugin": "^1.3.0",
38
43
  "ai": "5.0.124",
39
44
  "bun-types": "^1.3.11",
45
+ "oxfmt": "^0.41.0",
46
+ "oxlint": "^1.56.0",
47
+ "simple-git-hooks": "^2.13.1",
40
48
  "typescript": "^5.7.0"
41
49
  },
42
- "keywords": [
43
- "opencode",
44
- "anthropic",
45
- "claude",
46
- "ai-sdk",
47
- "provider"
48
- ],
49
- "license": "MIT"
50
+ "peerDependencies": {
51
+ "@ai-sdk/provider": ">=2.0.0"
52
+ },
53
+ "simple-git-hooks": {
54
+ "pre-commit": "bun run lint && bun run format:check"
55
+ }
50
56
  }
@@ -1,24 +0,0 @@
1
- import type { LanguageModelV2 } from "@ai-sdk/provider";
2
- export interface AgentSDKProviderOptions {
3
- /**
4
- * Provider name for logging. Defaults to "anthropic-agent-sdk".
5
- */
6
- name?: string;
7
- /**
8
- * Any additional options (passed through by OpenCode).
9
- */
10
- [key: string]: unknown;
11
- }
12
- export interface AgentSDKProvider {
13
- languageModel(modelId: string): LanguageModelV2;
14
- }
15
- /**
16
- * Create an Anthropic Agent SDK provider for use with OpenCode / Vercel AI SDK.
17
- *
18
- * Uses @anthropic-ai/claude-agent-sdk which handles authentication via
19
- * Claude Code's built-in OAuth flow — no API keys or billing headers needed.
20
- */
21
- export declare function createAgentSDK(options?: AgentSDKProviderOptions): AgentSDKProvider;
22
- export default createAgentSDK;
23
- export { AgentSDKModel } from "./agent-model.js";
24
- //# sourceMappingURL=agent-index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"agent-index.d.ts","sourceRoot":"","sources":["../src/agent-index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAEvD,MAAM,WAAW,uBAAuB;IACtC;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IAEb;;OAEG;IACH,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,CAAA;CAChD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,OAAO,GAAE,uBAA4B,GACpC,gBAAgB,CAQlB;AAED,eAAe,cAAc,CAAA;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA"}
@@ -1,302 +0,0 @@
1
- // src/agent-model.ts
2
- import {
3
- unstable_v2_createSession
4
- } from "@anthropic-ai/claude-agent-sdk";
5
- function mapFinishReason(reason) {
6
- switch (reason) {
7
- case "end_turn":
8
- return "stop";
9
- case "stop_sequence":
10
- return "stop";
11
- case "max_tokens":
12
- return "length";
13
- case "tool_use":
14
- return "tool-calls";
15
- default:
16
- return "unknown";
17
- }
18
- }
19
- function extractSystem(prompt) {
20
- const parts = [];
21
- for (const msg of prompt) {
22
- if (msg.role === "system")
23
- parts.push(msg.content);
24
- }
25
- return parts.length > 0 ? parts.join(`
26
-
27
- `) : undefined;
28
- }
29
- function extractMessages(prompt) {
30
- return prompt.filter((m) => m.role !== "system");
31
- }
32
- function formatLastMessage(msg) {
33
- if (msg.role === "user") {
34
- return msg.content.map((p) => {
35
- if (p.type === "text")
36
- return p.text;
37
- return "";
38
- }).filter(Boolean).join(`
39
- `);
40
- }
41
- if (msg.role === "tool") {
42
- return msg.content.map((p) => {
43
- const output = p.output;
44
- if (output.type === "text" || output.type === "error-text")
45
- return output.value;
46
- if (output.type === "json" || output.type === "error-json")
47
- return JSON.stringify(output.value);
48
- return String(output.value);
49
- }).join(`
50
- `);
51
- }
52
- return "";
53
- }
54
- function convertAgentStream(stream, modelId) {
55
- let idCounter = 0;
56
- return new ReadableStream({
57
- async start(controller) {
58
- try {
59
- let emittedMetadata = false;
60
- let firstAssistantTurnDone = false;
61
- for await (const msg of stream) {
62
- if (firstAssistantTurnDone) {
63
- if (msg.type === "result")
64
- break;
65
- continue;
66
- }
67
- if (msg.type === "assistant") {
68
- const message = msg.message;
69
- const content = message.content ?? [];
70
- if (!emittedMetadata && message.id) {
71
- emittedMetadata = true;
72
- controller.enqueue({
73
- type: "response-metadata",
74
- id: message.id,
75
- modelId: message.model ?? modelId,
76
- timestamp: new Date
77
- });
78
- }
79
- for (const block of content) {
80
- if (block.type === "text") {
81
- const id = `block-${idCounter++}`;
82
- controller.enqueue({ type: "text-start", id });
83
- controller.enqueue({ type: "text-delta", id, delta: block.text });
84
- controller.enqueue({ type: "text-end", id });
85
- } else if (block.type === "thinking") {
86
- const id = `block-${idCounter++}`;
87
- controller.enqueue({ type: "reasoning-start", id });
88
- controller.enqueue({ type: "reasoning-delta", id, delta: block.thinking });
89
- controller.enqueue({ type: "reasoning-end", id });
90
- }
91
- }
92
- }
93
- if (msg.type === "result") {
94
- const result = msg;
95
- const cacheRead = result.usage?.cache_read_input_tokens ?? 0;
96
- const cacheCreate = result.usage?.cache_creation_input_tokens ?? 0;
97
- controller.enqueue({
98
- type: "finish",
99
- finishReason: mapFinishReason(result.stop_reason),
100
- usage: {
101
- inputTokens: result.usage?.input_tokens ?? 0,
102
- outputTokens: result.usage?.output_tokens ?? 0,
103
- totalTokens: (result.usage?.input_tokens ?? 0) + (result.usage?.output_tokens ?? 0) + cacheRead + cacheCreate,
104
- cachedInputTokens: cacheRead
105
- },
106
- providerMetadata: {
107
- anthropic: {
108
- cacheCreationInputTokens: cacheCreate
109
- }
110
- }
111
- });
112
- firstAssistantTurnDone = true;
113
- break;
114
- }
115
- }
116
- } catch (error) {
117
- controller.enqueue({ type: "error", error });
118
- } finally {
119
- controller.close();
120
- }
121
- }
122
- });
123
- }
124
-
125
- class AgentSDKModel {
126
- providerName;
127
- specificationVersion = "v2";
128
- provider;
129
- modelId;
130
- supportedUrls = {};
131
- session = null;
132
- processedMessageCount = 0;
133
- lastSystemPrompt = undefined;
134
- constructor(modelId, providerName) {
135
- this.providerName = providerName;
136
- this.modelId = modelId;
137
- this.provider = providerName;
138
- }
139
- needsNewSession(system, messages) {
140
- if (!this.session)
141
- return true;
142
- if (system !== this.lastSystemPrompt)
143
- return true;
144
- if (messages.length < this.processedMessageCount)
145
- return true;
146
- return false;
147
- }
148
- createSession(system, tools) {
149
- const opts = {
150
- model: this.modelId,
151
- maxTurns: 100,
152
- includePartialMessages: true,
153
- permissionMode: "bypassPermissions",
154
- allowDangerouslySkipPermissions: true,
155
- persistSession: false,
156
- systemPrompt: system ?? "You are a helpful assistant.",
157
- tools: []
158
- };
159
- return unstable_v2_createSession(opts);
160
- }
161
- async doStream(options) {
162
- const system = extractSystem(options.prompt);
163
- const messages = extractMessages(options.prompt);
164
- const warnings = [];
165
- if (this.needsNewSession(system, messages)) {
166
- if (this.session) {
167
- try {
168
- this.session.close();
169
- } catch {}
170
- }
171
- this.session = this.createSession(system, options.tools);
172
- this.lastSystemPrompt = system;
173
- this.processedMessageCount = 0;
174
- }
175
- let messageToSend;
176
- if (this.processedMessageCount === 0) {
177
- const firstUser = messages.find((m) => m.role === "user");
178
- if (firstUser) {
179
- messageToSend = formatLastMessage(firstUser);
180
- this.processedMessageCount = messages.indexOf(firstUser) + 1;
181
- }
182
- } else {
183
- const newMessages = messages.slice(this.processedMessageCount);
184
- const toolMsg = newMessages.find((m) => m.role === "tool");
185
- if (toolMsg) {
186
- messageToSend = formatLastMessage(toolMsg);
187
- } else {
188
- const userMsg = [...newMessages].reverse().find((m) => m.role === "user");
189
- if (userMsg) {
190
- messageToSend = formatLastMessage(userMsg);
191
- }
192
- }
193
- this.processedMessageCount = messages.length;
194
- }
195
- if (!messageToSend) {
196
- messageToSend = "Continue.";
197
- }
198
- await this.session.send(messageToSend);
199
- const stream = convertAgentStream(this.session.stream(), this.modelId);
200
- const wrappedStream = new ReadableStream({
201
- async start(controller) {
202
- controller.enqueue({ type: "stream-start", warnings });
203
- const reader = stream.getReader();
204
- try {
205
- while (true) {
206
- const { done, value } = await reader.read();
207
- if (done)
208
- break;
209
- controller.enqueue(value);
210
- }
211
- } finally {
212
- reader.releaseLock();
213
- controller.close();
214
- }
215
- }
216
- });
217
- return {
218
- stream: wrappedStream,
219
- request: { body: messageToSend }
220
- };
221
- }
222
- async doGenerate(options) {
223
- const result = await this.doStream(options);
224
- const reader = result.stream.getReader();
225
- const content = [];
226
- let finishReason = "unknown";
227
- let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
228
- let responseId = "";
229
- let responseModel = this.modelId;
230
- let currentText = "";
231
- while (true) {
232
- const { done, value } = await reader.read();
233
- if (done)
234
- break;
235
- switch (value.type) {
236
- case "text-delta":
237
- currentText += value.delta ?? "";
238
- break;
239
- case "text-end":
240
- if (currentText) {
241
- content.push({ type: "text", text: currentText });
242
- currentText = "";
243
- }
244
- break;
245
- case "tool-call":
246
- content.push({
247
- type: "tool-call",
248
- toolCallId: value.toolCallId,
249
- toolName: value.toolName,
250
- input: value.input
251
- });
252
- break;
253
- case "finish":
254
- finishReason = value.finishReason;
255
- usage = value.usage ?? usage;
256
- break;
257
- case "response-metadata":
258
- responseId = value.id ?? "";
259
- responseModel = value.modelId ?? this.modelId;
260
- break;
261
- }
262
- }
263
- if (currentText) {
264
- content.push({ type: "text", text: currentText });
265
- }
266
- return {
267
- content,
268
- finishReason,
269
- usage,
270
- response: {
271
- id: responseId,
272
- modelId: responseModel,
273
- timestamp: new Date
274
- },
275
- warnings: []
276
- };
277
- }
278
- close() {
279
- if (this.session) {
280
- try {
281
- this.session.close();
282
- } catch {}
283
- this.session = null;
284
- }
285
- }
286
- }
287
-
288
- // src/agent-index.ts
289
- function createAgentSDK(options = {}) {
290
- const { name = "anthropic-agent-sdk", ...rest } = options;
291
- return {
292
- languageModel(modelId) {
293
- return new AgentSDKModel(modelId, name);
294
- }
295
- };
296
- }
297
- var agent_index_default = createAgentSDK;
298
- export {
299
- agent_index_default as default,
300
- createAgentSDK,
301
- AgentSDKModel
302
- };
@@ -1,34 +0,0 @@
1
- import type { LanguageModelV2, LanguageModelV2CallOptions } from "@ai-sdk/provider";
2
- type DoStreamResult = Awaited<ReturnType<LanguageModelV2["doStream"]>>;
3
- type DoGenerateResult = Awaited<ReturnType<LanguageModelV2["doGenerate"]>>;
4
- export declare class AgentSDKModel implements LanguageModelV2 {
5
- private providerName;
6
- readonly specificationVersion: "v2";
7
- readonly provider: string;
8
- readonly modelId: string;
9
- readonly supportedUrls: Record<string, RegExp[]>;
10
- private session;
11
- private processedMessageCount;
12
- private lastSystemPrompt;
13
- constructor(modelId: string, providerName: string);
14
- /**
15
- * Determine if we need a new session based on the prompt.
16
- * A new session is needed when:
17
- * - No session exists
18
- * - System prompt changed
19
- * - Message count decreased (new conversation)
20
- *
21
- * The session is REUSED across doStream() calls for the same conversation
22
- * (tool result continuations and multi-turn).
23
- */
24
- private needsNewSession;
25
- private createSession;
26
- doStream(options: LanguageModelV2CallOptions): Promise<DoStreamResult>;
27
- doGenerate(options: LanguageModelV2CallOptions): Promise<DoGenerateResult>;
28
- /**
29
- * Close the session and clean up resources.
30
- */
31
- close(): void;
32
- }
33
- export {};
34
- //# sourceMappingURL=agent-model.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"agent-model.d.ts","sourceRoot":"","sources":["../src/agent-model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,0BAA0B,EAO3B,MAAM,kBAAkB,CAAA;AAMzB,KAAK,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;AACtE,KAAK,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;AA6J1E,qBAAa,aAAc,YAAW,eAAe;IAajD,OAAO,CAAC,YAAY;IAZtB,QAAQ,CAAC,oBAAoB,EAAG,IAAI,CAAS;IAC7C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAK;IAErD,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,qBAAqB,CAAI;IACjC,OAAO,CAAC,gBAAgB,CAAgC;gBAItD,OAAO,EAAE,MAAM,EACP,YAAY,EAAE,MAAM;IAM9B;;;;;;;;;OASG;IACH,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,aAAa;IAoBf,QAAQ,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,cAAc,CAAC;IA2EtE,UAAU,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA+DhF;;OAEG;IACH,KAAK;CAMN"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=agent-test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"agent-test.d.ts","sourceRoot":"","sources":["../src/agent-test.ts"],"names":[],"mappings":""}
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env bun
2
- export {};
3
- //# sourceMappingURL=usage-cli.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"usage-cli.d.ts","sourceRoot":"","sources":["../src/usage-cli.ts"],"names":[],"mappings":""}
@@ -1,148 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- // src/credentials.ts
4
- import { readFileSync, existsSync } from "node:fs";
5
- import { join } from "node:path";
6
- import { homedir } from "node:os";
7
- function getCredentialsPath() {
8
- return join(homedir(), ".claude", ".credentials.json");
9
- }
10
- function readClaudeCredentials(path) {
11
- const credPath = path ?? getCredentialsPath();
12
- if (!existsSync(credPath))
13
- return null;
14
- try {
15
- const raw = readFileSync(credPath, "utf-8");
16
- const parsed = JSON.parse(raw);
17
- if (!parsed.claudeAiOauth?.accessToken)
18
- return null;
19
- return parsed.claudeAiOauth;
20
- } catch {
21
- return null;
22
- }
23
- }
24
- function isExpired(creds) {
25
- const buffer = 5 * 60 * 1000;
26
- return Date.now() > creds.expiresAt - buffer;
27
- }
28
-
29
- // src/usage.ts
30
- var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
31
- async function fetchUsage(credentialsPath) {
32
- const creds = readClaudeCredentials(credentialsPath);
33
- if (!creds)
34
- return null;
35
- if (isExpired(creds))
36
- return null;
37
- const resp = await fetch(USAGE_URL, {
38
- headers: {
39
- authorization: `Bearer ${creds.accessToken}`,
40
- "anthropic-beta": "claude-code-20250219,oauth-2025-04-20",
41
- "content-type": "application/json",
42
- "user-agent": "claude-cli/2.1.81 (external, sdk-cli)",
43
- "x-app": "cli"
44
- }
45
- });
46
- if (!resp.ok) {
47
- const body = await resp.text().catch(() => "");
48
- const msg = body.includes("message") ? JSON.parse(body)?.error?.message ?? body : `HTTP ${resp.status}`;
49
- throw new Error(`Usage API error: ${msg}`);
50
- }
51
- return resp.json();
52
- }
53
- function progressBar(utilization, width = 50) {
54
- const filled = Math.round(utilization / 100 * width);
55
- const bar = "█".repeat(filled) + " ".repeat(width - filled);
56
- return bar;
57
- }
58
- function formatReset(resetsAt) {
59
- if (!resetsAt)
60
- return "";
61
- const reset = new Date(resetsAt);
62
- const now = new Date;
63
- const diffMs = reset.getTime() - now.getTime();
64
- if (diffMs <= 0)
65
- return "Reset now";
66
- const diffMin = Math.floor(diffMs / 60000);
67
- const diffHour = Math.floor(diffMin / 60);
68
- const timeStr = reset.toLocaleTimeString("en-US", {
69
- hour: "numeric",
70
- minute: "2-digit",
71
- timeZoneName: "short"
72
- });
73
- const dateStr = reset.toLocaleDateString("en-US", {
74
- month: "short",
75
- day: "numeric"
76
- });
77
- if (diffHour < 24) {
78
- return `Resets ${timeStr}`;
79
- }
80
- return `Resets ${dateStr}, ${timeStr}`;
81
- }
82
- function formatUsage(data) {
83
- const lines = [];
84
- if (data.five_hour) {
85
- const pct = Math.round(data.five_hour.utilization);
86
- lines.push(" Current session");
87
- lines.push(` ${progressBar(pct)} ${pct}% used`);
88
- lines.push(` ${formatReset(data.five_hour.resets_at)}`);
89
- lines.push("");
90
- }
91
- if (data.seven_day) {
92
- const pct = Math.round(data.seven_day.utilization);
93
- lines.push(" Current week (all models)");
94
- lines.push(` ${progressBar(pct)} ${pct}% used`);
95
- lines.push(` ${formatReset(data.seven_day.resets_at)}`);
96
- lines.push("");
97
- }
98
- if (data.seven_day_sonnet) {
99
- const pct = Math.round(data.seven_day_sonnet.utilization);
100
- lines.push(" Current week (Sonnet only)");
101
- lines.push(` ${progressBar(pct)} ${pct}% used`);
102
- if (data.seven_day_sonnet.resets_at) {
103
- lines.push(` ${formatReset(data.seven_day_sonnet.resets_at)}`);
104
- }
105
- lines.push("");
106
- }
107
- if (data.seven_day_opus) {
108
- const pct = Math.round(data.seven_day_opus.utilization);
109
- lines.push(" Current week (Opus only)");
110
- lines.push(` ${progressBar(pct)} ${pct}% used`);
111
- if (data.seven_day_opus.resets_at) {
112
- lines.push(` ${formatReset(data.seven_day_opus.resets_at)}`);
113
- }
114
- lines.push("");
115
- }
116
- if (data.extra_usage) {
117
- if (data.extra_usage.is_enabled) {
118
- const pct = Math.round(data.extra_usage.utilization ?? 0);
119
- lines.push(" Extra usage");
120
- lines.push(` ${progressBar(pct)} ${pct}% used`);
121
- if (data.extra_usage.monthly_limit != null) {
122
- lines.push(` $${data.extra_usage.used_credits?.toFixed(2) ?? "0.00"} / $${data.extra_usage.monthly_limit.toFixed(2)}`);
123
- }
124
- } else {
125
- lines.push(" Extra usage: disabled");
126
- }
127
- lines.push("");
128
- }
129
- return lines.join(`
130
- `);
131
- }
132
-
133
- // src/usage-cli.ts
134
- var data;
135
- try {
136
- data = await fetchUsage();
137
- } catch (e) {
138
- console.error(e.message ?? "Could not fetch usage.");
139
- process.exit(1);
140
- }
141
- if (!data) {
142
- console.error("Could not fetch usage. Make sure you're logged into Claude Code (~/.claude/.credentials.json).");
143
- process.exit(1);
144
- }
145
- console.log("");
146
- console.log(" Claude Subscription Usage");
147
- console.log(" " + "─".repeat(52));
148
- console.log(formatUsage(data));