@suncreation/opencode-claude-patch 0.1.0

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 ADDED
@@ -0,0 +1,105 @@
1
+ # opencode-anthropic-oauth-patch
2
+
3
+ Installable OpenCode plugin for sanitizing Anthropic OAuth / Claude request shape, with helper exports for lower-level integrations.
4
+
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
+
7
+ ## What it patches automatically as an OpenCode plugin
8
+
9
+ - Removes `thinking`, `output_config`, and related Anthropic-problematic option fields from OpenCode provider options
10
+ - Rewrites OpenCode system prompt strings into a single Claude-Code-compatible system string
11
+ - Sets Claude-Code-compatible Anthropic headers by default
12
+ - Installs a targeted global `fetch` patch that sanitizes outbound Anthropic OAuth message bodies, including final `system[*].cache_control` removal
13
+
14
+ ## How the automatic patch works
15
+
16
+ The root plugin uses two layers:
17
+
18
+ 1. OpenCode hooks (`chat.params`, `chat.headers`, `experimental.chat.system.transform`) for public-ABI-safe fixes
19
+ 2. a narrowly targeted `globalThis.fetch` wrapper that only patches `https://api.anthropic.com/v1/messages...` requests carrying `Authorization: Bearer ...`
20
+
21
+ That second layer is what makes the package automatic for the `cache_control` problem too.
22
+
23
+ ## Install
24
+
25
+ Add the package to your `opencode.json` plugin list:
26
+
27
+ ```json
28
+ {
29
+ "$schema": "https://opencode.ai/config.json",
30
+ "plugin": [
31
+ "@suncreation/opencode-claude-patch"
32
+ ]
33
+ }
34
+ ```
35
+
36
+ Install it from this repository:
37
+
38
+ ```bash
39
+ npm install /Users/admin/Projects/subscription-llm/npm/opencode-anthropic-oauth-patch
40
+ ```
41
+
42
+ If you later publish it, keep the same `plugin` entry and install with:
43
+
44
+ ```bash
45
+ npm install @suncreation/opencode-claude-patch
46
+ ```
47
+
48
+ OpenCode will auto-install npm plugins referenced in `opencode.json` at startup.
49
+
50
+ ## What the root package export does
51
+
52
+ The root package export is an OpenCode plugin function. Once installed and listed in `opencode.json`, it auto-applies:
53
+
54
+ - `chat.params` patching for Anthropic/Claude models
55
+ - `chat.headers` patching for Claude-compatible headers
56
+ - `experimental.chat.system.transform` patching for a single Claude-Code-style system prompt
57
+ - a targeted `globalThis.fetch` patch for final Anthropic OAuth request-body cleanup
58
+
59
+ No extra wiring is needed.
60
+
61
+ ## Optional environment toggles
62
+
63
+ ```bash
64
+ OPENCODE_ANTHROPIC_OAUTH_SET_HEADERS=0
65
+ OPENCODE_ANTHROPIC_OAUTH_SYSTEM_PREFIX="You are Claude Code, Anthropic's official CLI for Claude."
66
+ ```
67
+
68
+ ## Helper subpath for lower-level integrations
69
+
70
+ ```js
71
+ import {
72
+ createAnthropicOAuthPatchedFetch,
73
+ mergePatchHeaders,
74
+ normalizeAnthropicSystemStrings,
75
+ sanitizeAnthropicOptions
76
+ } from "@suncreation/opencode-claude-patch/helpers";
77
+
78
+ // if published:
79
+ // import { ... } from "@suncreation/opencode-anthropic-oauth-patch/helpers";
80
+
81
+ const options = sanitizeAnthropicOptions({
82
+ thinking: { type: "adaptive" },
83
+ output_config: { effort: "max" }
84
+ });
85
+
86
+ const headers = mergePatchHeaders({});
87
+ const system = normalizeAnthropicSystemStrings([
88
+ "Be concise.",
89
+ "Answer in Korean."
90
+ ]);
91
+
92
+ const patchedFetch = createAnthropicOAuthPatchedFetch(fetch, {
93
+ ensureClaudeCodeSystem: true,
94
+ normalizeSystemToUserMessage: true
95
+ });
96
+
97
+ console.log(options, headers, system, typeof patchedFetch);
98
+ ```
99
+
100
+ ## Notes
101
+
102
+ - The root export is safe for `opencode.json -> plugin` installation.
103
+ - The `./helpers` subpath is for deeper interceptors or forks.
104
+ - It does not mint OAuth tokens or bypass account-level/provider policy restrictions.
105
+ - This package is targeted at Claude OAuth / oh-my-opencode-style setups. If you use Anthropic API keys and want native reasoning features, do not enable it globally.
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@suncreation/opencode-claude-patch",
3
+ "version": "0.1.0",
4
+ "description": "Reusable Anthropic OAuth request sanitizer for OpenCode and similar tools",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.d.ts",
11
+ "import": "./src/index.js"
12
+ },
13
+ "./helpers": {
14
+ "types": "./src/helpers.d.ts",
15
+ "import": "./src/helpers.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "src",
20
+ "README.md"
21
+ ],
22
+ "scripts": {
23
+ "test": "node --test"
24
+ },
25
+ "keywords": [
26
+ "anthropic",
27
+ "oauth",
28
+ "opencode",
29
+ "claude",
30
+ "patch"
31
+ ],
32
+ "license": "MIT"
33
+ ,"publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }
@@ -0,0 +1,29 @@
1
+ export interface PatchOptions {
2
+ ensureClaudeCodeSystem?: boolean;
3
+ normalizeSystemToUserMessage?: boolean;
4
+ systemPrefix?: string;
5
+ }
6
+
7
+ export declare const DEFAULT_SYSTEM_PREFIX: string;
8
+ export declare const DEFAULT_PATCH_HEADERS: Record<string, string>;
9
+
10
+ export declare function sanitizeAnthropicOAuthBody<T>(body: T, options?: PatchOptions): T;
11
+ export declare function sanitizeAnthropicOptions<T>(options: T): T;
12
+ export declare function normalizeAnthropicSystemStrings(system: unknown, options?: PatchOptions): string[];
13
+ export declare function mergePatchHeaders(
14
+ headers?: Record<string, string>,
15
+ overrides?: Record<string, string>
16
+ ): Record<string, string>;
17
+ export declare function isAnthropicClaudeModel(model: unknown): boolean;
18
+ export declare function isOpenCodeAnthropicPatchTarget(input: unknown): boolean;
19
+ export declare function shouldPatchAnthropicOAuthRequest(url: string, init?: RequestInit): boolean;
20
+ export declare function patchAnthropicOAuthRequest(
21
+ url: string,
22
+ init?: RequestInit,
23
+ options?: PatchOptions
24
+ ): { url: string; init: RequestInit; patched: boolean; body?: Record<string, unknown> };
25
+ export declare function createAnthropicOAuthPatchedFetch(
26
+ fetchImpl: typeof fetch,
27
+ options?: PatchOptions
28
+ ): typeof fetch;
29
+ export declare function installGlobalAnthropicOAuthFetchPatch(options?: PatchOptions): boolean;
package/src/helpers.js ADDED
@@ -0,0 +1,260 @@
1
+ const DEFAULT_SYSTEM_PREFIX = "You are Claude Code, Anthropic's official CLI for Claude.";
2
+
3
+ const DEFAULT_PATCH_HEADERS = {
4
+ "anthropic-beta": "oauth-2025-04-20,interleaved-thinking-2025-05-14,claude-code-20250219",
5
+ "anthropic-version": "2023-06-01",
6
+ "user-agent": "claude-cli/2.1.2 (external, cli)"
7
+ };
8
+
9
+ const FETCH_PATCH_MARKER = Symbol.for("opencode.anthropic.oauth.patch.fetch");
10
+
11
+ function isPlainObject(value) {
12
+ return value !== null && typeof value === "object" && !Array.isArray(value);
13
+ }
14
+
15
+ function deepClone(value) {
16
+ return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
17
+ }
18
+
19
+ function isAnthropicMessagesUrl(url) {
20
+ if (typeof url !== "string") {
21
+ return false;
22
+ }
23
+
24
+ return /^https:\/\/api\.anthropic\.com\/v1\/messages(?:\?|$)/.test(url);
25
+ }
26
+
27
+ function hasOAuthBearer(headers) {
28
+ if (!headers) {
29
+ return false;
30
+ }
31
+
32
+ if (typeof Headers !== "undefined" && headers instanceof Headers) {
33
+ const authorization = headers.get("authorization");
34
+ return typeof authorization === "string" && authorization.toLowerCase().startsWith("bearer ");
35
+ }
36
+
37
+ if (Array.isArray(headers)) {
38
+ for (const [key, value] of headers) {
39
+ if (String(key).toLowerCase() === "authorization") {
40
+ return String(value).toLowerCase().startsWith("bearer ");
41
+ }
42
+ }
43
+ return false;
44
+ }
45
+
46
+ if (isPlainObject(headers)) {
47
+ for (const [key, value] of Object.entries(headers)) {
48
+ if (key.toLowerCase() === "authorization") {
49
+ return typeof value === "string" && value.toLowerCase().startsWith("bearer ");
50
+ }
51
+ }
52
+ }
53
+
54
+ return false;
55
+ }
56
+
57
+ function extractBody(init) {
58
+ if (!init || !("body" in init)) {
59
+ return { body: undefined, bodyType: "missing" };
60
+ }
61
+
62
+ if (typeof init.body === "string") {
63
+ return { body: JSON.parse(init.body), bodyType: "json-string" };
64
+ }
65
+
66
+ if (isPlainObject(init.body)) {
67
+ return { body: deepClone(init.body), bodyType: "object" };
68
+ }
69
+
70
+ return { body: undefined, bodyType: "unsupported" };
71
+ }
72
+
73
+ function stripKnownAnthropicProblemFields(node) {
74
+ if (Array.isArray(node)) {
75
+ return node.map(stripKnownAnthropicProblemFields);
76
+ }
77
+
78
+ if (!isPlainObject(node)) {
79
+ return node;
80
+ }
81
+
82
+ const next = {};
83
+ for (const [key, value] of Object.entries(node)) {
84
+ if (
85
+ key === "thinking" ||
86
+ key === "output_config" ||
87
+ key === "outputConfig" ||
88
+ key === "cache_control" ||
89
+ key === "cacheControl"
90
+ ) {
91
+ continue;
92
+ }
93
+
94
+ if (key === "effort" && isPlainObject(node) && ("thinking" in node || "output_config" in node || "outputConfig" in node)) {
95
+ continue;
96
+ }
97
+
98
+ next[key] = stripKnownAnthropicProblemFields(value);
99
+ }
100
+ return next;
101
+ }
102
+
103
+ function mergeSystemTextIntoFirstUserMessage(messages, text) {
104
+ if (!text) {
105
+ return messages;
106
+ }
107
+
108
+ const injected = `<SYSTEM_INSTRUCTIONS>\n${text}\n</SYSTEM_INSTRUCTIONS>`;
109
+ const nextMessages = Array.isArray(messages) ? messages.map((message) => ({ ...message })) : [];
110
+
111
+ if (nextMessages.length > 0 && nextMessages[0].role === "user" && typeof nextMessages[0].content === "string") {
112
+ nextMessages[0].content = `${injected}\n\n${nextMessages[0].content}`;
113
+ return nextMessages;
114
+ }
115
+
116
+ nextMessages.unshift({ role: "user", content: injected });
117
+ return nextMessages;
118
+ }
119
+
120
+ export function sanitizeAnthropicOAuthBody(body, options = {}) {
121
+ if (!isPlainObject(body)) {
122
+ return body;
123
+ }
124
+
125
+ const {
126
+ ensureClaudeCodeSystem = false,
127
+ normalizeSystemToUserMessage = false,
128
+ systemPrefix = DEFAULT_SYSTEM_PREFIX
129
+ } = options;
130
+
131
+ const nextBody = stripKnownAnthropicProblemFields(deepClone(body));
132
+
133
+ if (ensureClaudeCodeSystem || normalizeSystemToUserMessage) {
134
+ const systemText = [];
135
+
136
+ if (Array.isArray(nextBody.system)) {
137
+ for (const entry of nextBody.system) {
138
+ if (isPlainObject(entry) && typeof entry.text === "string" && entry.text !== systemPrefix) {
139
+ systemText.push(entry.text);
140
+ }
141
+ }
142
+ }
143
+
144
+ nextBody.system = [{ type: "text", text: systemPrefix }];
145
+
146
+ if (normalizeSystemToUserMessage) {
147
+ nextBody.messages = mergeSystemTextIntoFirstUserMessage(nextBody.messages, systemText.join("\n\n"));
148
+ }
149
+ }
150
+
151
+ return nextBody;
152
+ }
153
+
154
+ export function sanitizeAnthropicOptions(options) {
155
+ if (!isPlainObject(options)) {
156
+ return options;
157
+ }
158
+
159
+ return stripKnownAnthropicProblemFields(deepClone(options));
160
+ }
161
+
162
+ export function normalizeAnthropicSystemStrings(system, options = {}) {
163
+ const { systemPrefix = DEFAULT_SYSTEM_PREFIX } = options;
164
+ const values = Array.isArray(system) ? system.filter((entry) => typeof entry === "string" && entry.trim()) : [];
165
+ const extra = values.filter((entry) => entry !== systemPrefix).join("\n\n");
166
+
167
+ if (!extra) {
168
+ return [systemPrefix];
169
+ }
170
+
171
+ return [`${systemPrefix}\n\n<SYSTEM_INSTRUCTIONS>\n${extra}\n</SYSTEM_INSTRUCTIONS>`];
172
+ }
173
+
174
+ export function mergePatchHeaders(headers = {}, overrides = {}) {
175
+ return {
176
+ ...headers,
177
+ ...DEFAULT_PATCH_HEADERS,
178
+ ...overrides
179
+ };
180
+ }
181
+
182
+ export function isAnthropicClaudeModel(model) {
183
+ if (!isPlainObject(model)) {
184
+ return false;
185
+ }
186
+
187
+ const providerID = typeof model.providerID === "string" ? model.providerID.toLowerCase() : "";
188
+ const modelID = typeof model.id === "string" ? model.id.toLowerCase() : "";
189
+ const api = isPlainObject(model.api) ? model.api : {};
190
+ const apiID = typeof api.id === "string" ? api.id.toLowerCase() : "";
191
+ const apiNpm = typeof api.npm === "string" ? api.npm.toLowerCase() : "";
192
+
193
+ return (
194
+ providerID.includes("anthropic") ||
195
+ modelID.includes("claude") ||
196
+ apiID.includes("claude") ||
197
+ apiID.includes("anthropic") ||
198
+ apiNpm === "@ai-sdk/anthropic" ||
199
+ apiNpm === "@ai-sdk/google-vertex/anthropic"
200
+ );
201
+ }
202
+
203
+ export function isOpenCodeAnthropicPatchTarget(input) {
204
+ return isPlainObject(input) && isAnthropicClaudeModel(input.model);
205
+ }
206
+
207
+ export function shouldPatchAnthropicOAuthRequest(url, init = {}) {
208
+ return isAnthropicMessagesUrl(url) && hasOAuthBearer(init.headers);
209
+ }
210
+
211
+ export function patchAnthropicOAuthRequest(url, init = {}, options = {}) {
212
+ if (!shouldPatchAnthropicOAuthRequest(url, init)) {
213
+ return { url, init, patched: false };
214
+ }
215
+
216
+ const { body, bodyType } = extractBody(init);
217
+ if (!body || bodyType === "unsupported") {
218
+ return { url, init, patched: false };
219
+ }
220
+
221
+ const sanitizedBody = sanitizeAnthropicOAuthBody(body, options);
222
+ const nextInit = { ...init };
223
+
224
+ if (bodyType === "json-string") {
225
+ nextInit.body = JSON.stringify(sanitizedBody);
226
+ } else {
227
+ nextInit.body = sanitizedBody;
228
+ }
229
+
230
+ return { url, init: nextInit, patched: true, body: sanitizedBody };
231
+ }
232
+
233
+ export function createAnthropicOAuthPatchedFetch(fetchImpl, options = {}) {
234
+ if (typeof fetchImpl !== "function") {
235
+ throw new TypeError("fetchImpl must be a function");
236
+ }
237
+
238
+ const patchedFetch = async function patchedFetch(url, init = {}) {
239
+ const patched = patchAnthropicOAuthRequest(url, init, options);
240
+ return fetchImpl(patched.url, patched.init);
241
+ };
242
+
243
+ patchedFetch[FETCH_PATCH_MARKER] = true;
244
+ return patchedFetch;
245
+ }
246
+
247
+ export function installGlobalAnthropicOAuthFetchPatch(options = {}) {
248
+ if (typeof globalThis.fetch !== "function") {
249
+ return false;
250
+ }
251
+
252
+ if (globalThis.fetch[FETCH_PATCH_MARKER]) {
253
+ return false;
254
+ }
255
+
256
+ globalThis.fetch = createAnthropicOAuthPatchedFetch(globalThis.fetch.bind(globalThis), options);
257
+ return true;
258
+ }
259
+
260
+ export { DEFAULT_PATCH_HEADERS, DEFAULT_SYSTEM_PREFIX, FETCH_PATCH_MARKER };
package/src/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ declare const OpenCodeAnthropicOAuthPatch: (...args: any[]) => Promise<Record<string, unknown>>;
2
+
3
+ export default OpenCodeAnthropicOAuthPatch;
package/src/index.js ADDED
@@ -0,0 +1,54 @@
1
+ import {
2
+ DEFAULT_SYSTEM_PREFIX,
3
+ installGlobalAnthropicOAuthFetchPatch,
4
+ isOpenCodeAnthropicPatchTarget,
5
+ mergePatchHeaders,
6
+ normalizeAnthropicSystemStrings,
7
+ sanitizeAnthropicOptions
8
+ } from "./helpers.js";
9
+
10
+ function getPluginOptions() {
11
+ return {
12
+ systemPrefix: process.env.OPENCODE_ANTHROPIC_OAUTH_SYSTEM_PREFIX || DEFAULT_SYSTEM_PREFIX,
13
+ setClaudeHeaders: process.env.OPENCODE_ANTHROPIC_OAUTH_SET_HEADERS !== "0"
14
+ };
15
+ }
16
+
17
+ export default async function OpenCodeAnthropicOAuthPatch() {
18
+ const pluginOptions = getPluginOptions();
19
+ installGlobalAnthropicOAuthFetchPatch({
20
+ ensureClaudeCodeSystem: true,
21
+ normalizeSystemToUserMessage: true,
22
+ systemPrefix: pluginOptions.systemPrefix
23
+ });
24
+
25
+ return {
26
+ async "chat.params"(input, output) {
27
+ if (!isOpenCodeAnthropicPatchTarget(input)) {
28
+ return;
29
+ }
30
+
31
+ output.options = sanitizeAnthropicOptions(output.options);
32
+ },
33
+
34
+ async "chat.headers"(input, output) {
35
+ if (!isOpenCodeAnthropicPatchTarget(input) || !pluginOptions.setClaudeHeaders) {
36
+ return;
37
+ }
38
+
39
+ output.headers = mergePatchHeaders(output.headers);
40
+ },
41
+
42
+ async "experimental.chat.system.transform"(input, output) {
43
+ if (!isOpenCodeAnthropicPatchTarget(input)) {
44
+ return;
45
+ }
46
+
47
+ const nextSystem = normalizeAnthropicSystemStrings(output.system, {
48
+ systemPrefix: pluginOptions.systemPrefix
49
+ });
50
+ output.system.length = 0;
51
+ output.system.push(...nextSystem);
52
+ }
53
+ };
54
+ }