@suncreation/opencode-claude-patch 0.1.0 → 0.1.1

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
@@ -1,11 +1,13 @@
1
1
  # opencode-anthropic-oauth-patch
2
2
 
3
- Installable OpenCode plugin for sanitizing Anthropic OAuth / Claude request shape, with helper exports for lower-level integrations.
3
+ Installable OpenCode plugin that restores Anthropic provider visibility, reads the working OpenCode OAuth token file, refreshes Anthropic access tokens, and patches Claude request shape for OpenCode/oh-my-opencode.
4
4
 
5
5
  This is meant for OpenCode/oh-my-opencode-style integrations where Claude OAuth requests break because extra Anthropic-incompatible fields are injected into the request path.
6
6
 
7
7
  ## What it patches automatically as an OpenCode plugin
8
8
 
9
+ - Registers an Anthropic auth/provider bridge for OpenCode using `~/.local/share/opencode/auth.json`
10
+ - Refreshes Anthropic OAuth access tokens through `https://console.anthropic.com/v1/oauth/token`
9
11
  - Removes `thinking`, `output_config`, and related Anthropic-problematic option fields from OpenCode provider options
10
12
  - Rewrites OpenCode system prompt strings into a single Claude-Code-compatible system string
11
13
  - Sets Claude-Code-compatible Anthropic headers by default
@@ -36,7 +38,7 @@ Add the package to your `opencode.json` plugin list:
36
38
  Install it from this repository:
37
39
 
38
40
  ```bash
39
- npm install /Users/admin/Projects/subscription-llm/npm/opencode-anthropic-oauth-patch
41
+ npm install /Users/admin/Projects/npm/opencode-anthropic-oauth-patch
40
42
  ```
41
43
 
42
44
  If you later publish it, keep the same `plugin` entry and install with:
@@ -51,6 +53,7 @@ OpenCode will auto-install npm plugins referenced in `opencode.json` at startup.
51
53
 
52
54
  The root package export is an OpenCode plugin function. Once installed and listed in `opencode.json`, it auto-applies:
53
55
 
56
+ - Anthropic auth/provider loading backed by the OpenCode OAuth token file
54
57
  - `chat.params` patching for Anthropic/Claude models
55
58
  - `chat.headers` patching for Claude-compatible headers
56
59
  - `experimental.chat.system.transform` patching for a single Claude-Code-style system prompt
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@suncreation/opencode-claude-patch",
3
- "version": "0.1.0",
4
- "description": "Reusable Anthropic OAuth request sanitizer for OpenCode and similar tools",
3
+ "version": "0.1.1",
4
+ "description": "Anthropic OAuth provider bridge and Claude request patch for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./src/index.d.ts",
package/src/helpers.d.ts CHANGED
@@ -27,3 +27,7 @@ export declare function createAnthropicOAuthPatchedFetch(
27
27
  options?: PatchOptions
28
28
  ): typeof fetch;
29
29
  export declare function installGlobalAnthropicOAuthFetchPatch(options?: PatchOptions): boolean;
30
+ export declare function getOAuthAccessToken(
31
+ provider: "anthropic",
32
+ options?: { authPath?: string }
33
+ ): Promise<string>;
package/src/helpers.js CHANGED
@@ -1,4 +1,9 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, resolve } from "node:path";
4
+
1
5
  const DEFAULT_SYSTEM_PREFIX = "You are Claude Code, Anthropic's official CLI for Claude.";
6
+ const DEFAULT_AUTH_PATH = resolve(homedir(), ".local/share/opencode/auth.json");
2
7
 
3
8
  const DEFAULT_PATCH_HEADERS = {
4
9
  "anthropic-beta": "oauth-2025-04-20,interleaved-thinking-2025-05-14,claude-code-20250219",
@@ -6,6 +11,13 @@ const DEFAULT_PATCH_HEADERS = {
6
11
  "user-agent": "claude-cli/2.1.2 (external, cli)"
7
12
  };
8
13
 
14
+ const PROVIDER_CONFIG = {
15
+ anthropic: {
16
+ refreshUrl: "https://console.anthropic.com/v1/oauth/token",
17
+ clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
18
+ }
19
+ };
20
+
9
21
  const FETCH_PATCH_MARKER = Symbol.for("opencode.anthropic.oauth.patch.fetch");
10
22
 
11
23
  function isPlainObject(value) {
@@ -16,6 +28,104 @@ function deepClone(value) {
16
28
  return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
17
29
  }
18
30
 
31
+ function resolveAuthPath(authPath) {
32
+ return authPath ? resolve(String(authPath).replace(/^~(?=$|\/)/, homedir())) : DEFAULT_AUTH_PATH;
33
+ }
34
+
35
+ function isTokenValid(tokenData) {
36
+ if (!isPlainObject(tokenData)) {
37
+ return false;
38
+ }
39
+
40
+ const access = tokenData.access;
41
+ const expires = tokenData.expires;
42
+ return typeof access === "string" && typeof expires === "number" && expires > Date.now() + 60_000;
43
+ }
44
+
45
+ async function readAuthData(authPath) {
46
+ try {
47
+ const raw = await readFile(authPath, "utf8");
48
+ const parsed = JSON.parse(raw);
49
+ return isPlainObject(parsed) ? parsed : {};
50
+ } catch {
51
+ return {};
52
+ }
53
+ }
54
+
55
+ async function writeAuthData(authPath, data) {
56
+ await mkdir(dirname(authPath), { recursive: true });
57
+ await writeFile(authPath, JSON.stringify(data, null, 2), "utf8");
58
+ }
59
+
60
+ function normalizeRefreshedToken(refreshed, previous = {}) {
61
+ const nowMs = Date.now();
62
+ let expires = refreshed.expires;
63
+ if (typeof expires !== "number") {
64
+ if (typeof refreshed.expires_at === "number") {
65
+ expires = refreshed.expires_at;
66
+ } else if (typeof refreshed.expires_in === "number") {
67
+ expires = nowMs + refreshed.expires_in * 1000;
68
+ } else if (typeof previous.expires === "number") {
69
+ expires = previous.expires;
70
+ } else {
71
+ expires = nowMs + 3600_000;
72
+ }
73
+ }
74
+
75
+ return {
76
+ ...previous,
77
+ access: refreshed.access ?? refreshed.access_token ?? previous.access,
78
+ refresh: refreshed.refresh ?? refreshed.refresh_token ?? previous.refresh,
79
+ expires: Math.trunc(expires)
80
+ };
81
+ }
82
+
83
+ async function refreshAccessToken(provider, providerData, authPath) {
84
+ const config = PROVIDER_CONFIG[provider];
85
+ const refreshToken = providerData?.refresh;
86
+ if (!config || typeof refreshToken !== "string" || !refreshToken) {
87
+ throw new Error(`Missing refresh token for provider '${provider}'`);
88
+ }
89
+
90
+ const response = await fetch(config.refreshUrl, {
91
+ method: "POST",
92
+ headers: { "content-type": "application/json" },
93
+ body: JSON.stringify({
94
+ grant_type: "refresh_token",
95
+ refresh_token: refreshToken,
96
+ client_id: config.clientId
97
+ })
98
+ });
99
+
100
+ if (!response.ok) {
101
+ throw new Error(`Token refresh failed: ${response.status}`);
102
+ }
103
+
104
+ const refreshed = await response.json();
105
+ const merged = normalizeRefreshedToken(isPlainObject(refreshed) ? refreshed : {}, providerData);
106
+ if (typeof merged.access !== "string" || !merged.access) {
107
+ throw new Error(`Refresh succeeded but no access token for provider '${provider}'`);
108
+ }
109
+
110
+ const authData = await readAuthData(authPath);
111
+ authData[provider] = merged;
112
+ await writeAuthData(authPath, authData);
113
+ return merged;
114
+ }
115
+
116
+ export async function getOAuthAccessToken(provider, options = {}) {
117
+ const authPath = resolveAuthPath(options.authPath);
118
+ const authData = await readAuthData(authPath);
119
+ const providerData = isPlainObject(authData[provider]) ? authData[provider] : {};
120
+
121
+ if (isTokenValid(providerData)) {
122
+ return providerData.access;
123
+ }
124
+
125
+ const refreshed = await refreshAccessToken(provider, providerData, authPath);
126
+ return refreshed.access;
127
+ }
128
+
19
129
  function isAnthropicMessagesUrl(url) {
20
130
  if (typeof url !== "string") {
21
131
  return false;
package/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  DEFAULT_SYSTEM_PREFIX,
3
+ getOAuthAccessToken,
3
4
  installGlobalAnthropicOAuthFetchPatch,
4
5
  isOpenCodeAnthropicPatchTarget,
5
6
  mergePatchHeaders,
@@ -23,6 +24,78 @@ export default async function OpenCodeAnthropicOAuthPatch() {
23
24
  });
24
25
 
25
26
  return {
27
+ auth: {
28
+ provider: "anthropic",
29
+ async loader(_getAuth, provider) {
30
+ for (const model of Object.values(provider.models ?? {})) {
31
+ model.cost = {
32
+ input: 0,
33
+ output: 0,
34
+ cache: {
35
+ read: 0,
36
+ write: 0
37
+ }
38
+ };
39
+ }
40
+
41
+ return {
42
+ apiKey: "",
43
+ async fetch(input, init) {
44
+ const token = await getOAuthAccessToken("anthropic");
45
+ const requestInit = init ?? {};
46
+ const requestHeaders = new Headers();
47
+
48
+ if (input instanceof Request) {
49
+ input.headers.forEach((value, key) => {
50
+ requestHeaders.set(key, value);
51
+ });
52
+ }
53
+
54
+ if (requestInit.headers) {
55
+ if (requestInit.headers instanceof Headers) {
56
+ requestInit.headers.forEach((value, key) => {
57
+ requestHeaders.set(key, value);
58
+ });
59
+ } else if (Array.isArray(requestInit.headers)) {
60
+ for (const [key, value] of requestInit.headers) {
61
+ if (typeof value !== "undefined") {
62
+ requestHeaders.set(key, String(value));
63
+ }
64
+ }
65
+ } else {
66
+ for (const [key, value] of Object.entries(requestInit.headers)) {
67
+ if (typeof value !== "undefined") {
68
+ requestHeaders.set(key, String(value));
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ requestHeaders.set("authorization", `Bearer ${token}`);
75
+ requestHeaders.delete("x-api-key");
76
+ requestHeaders.delete("Authorization");
77
+
78
+ const mergedHeaders = mergePatchHeaders(Object.fromEntries(requestHeaders.entries()));
79
+ const patchedFetch = installGlobalAnthropicOAuthFetchPatch({
80
+ ensureClaudeCodeSystem: true,
81
+ normalizeSystemToUserMessage: true,
82
+ systemPrefix: pluginOptions.systemPrefix
83
+ })
84
+ ? globalThis.fetch
85
+ : globalThis.fetch;
86
+
87
+ return patchedFetch(input, {
88
+ ...requestInit,
89
+ headers: {
90
+ ...mergedHeaders,
91
+ authorization: `Bearer ${token}`
92
+ }
93
+ });
94
+ }
95
+ };
96
+ }
97
+ },
98
+
26
99
  async "chat.params"(input, output) {
27
100
  if (!isOpenCodeAnthropicPatchTarget(input)) {
28
101
  return;