opencode-fixes-huihui 0.1.4-beta.1 → 0.1.4-beta.2

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,6 +1,9 @@
1
1
  # opencode-fixes-huihui
2
2
 
3
- opencode 单插件版本:统一处理 sticky session headers 和 `promptCacheKey`,用于稳定会话路由与缓存命中。
3
+ 统一插件包:默认导出组合插件,同时包含:
4
+
5
+ - sticky session headers + `promptCacheKey` 注入
6
+ - Anthropic 渠道 `metadata.user_id` 缓存注入
4
7
 
5
8
  ## 安装
6
9
 
@@ -10,44 +13,75 @@ npm i opencode-fixes-huihui
10
13
 
11
14
  ## 使用
12
15
 
16
+ 默认导出是组合插件(推荐):
17
+
13
18
  ```ts
14
- import StickySessionPlugin from "opencode-fixes-huihui";
19
+ import CombinedPlugin from "opencode-fixes-huihui";
15
20
 
16
21
  export default {
17
- plugins: [StickySessionPlugin],
22
+ plugins: [CombinedPlugin],
18
23
  };
19
24
  ```
20
25
 
21
- 也可使用具名导出:
26
+ 也支持具名导出:
22
27
 
23
28
  ```ts
24
- import { StickySessionPlugin } from "opencode-fixes-huihui";
29
+ import {
30
+ CombinedPlugin,
31
+ StickySessionPlugin,
32
+ AnthropicCache,
33
+ AnthropicUltraCache,
34
+ FoxcodeAwsCache,
35
+ createAnthropicCachePlugin,
36
+ } from "opencode-fixes-huihui";
25
37
  ```
26
38
 
27
- ## 行为
39
+ ## 功能说明
40
+
41
+ ### 1) Sticky session(`StickySessionPlugin`)
42
+
43
+ 在 `chat.params` 阶段执行,仅对 `@ai-sdk/openai` 或 `isCodex` provider 生效。
44
+
45
+ 会话值优先级:
46
+
47
+ 1. `OPENCODE_PROMPT_CACHE_KEY`
48
+ 2. `OPENCODE_STICKY_SESSION_ID`
49
+ 3. 现有 headers:`x-session-id` / `conversation_id` / `session_id`
50
+ 4. `input.sessionID`
51
+
52
+ 写入位置:
53
+
54
+ - `input.model.headers.x-session-id`
55
+ - `input.model.headers.conversation_id`
56
+ - `input.model.headers.session_id`
57
+ - `output.options.promptCacheKey`
58
+
59
+ 可选环境变量:
60
+
61
+ - `OPENCODE_PROMPT_CACHE_KEY`:强制覆盖会话值(最高优先级)
62
+ - `OPENCODE_STICKY_SESSION_ID`:次优先级覆盖会话值
63
+
64
+ ### 2) Anthropic cache(`AnthropicCache` 系列)
65
+
66
+ 在 auth loader 的请求 `fetch` 中拦截 Anthropic POST JSON 请求,自动补齐:
67
+
68
+ - `metadata.user_id = user_{projectId}_account__session_{sessionId}`
28
69
 
29
- 插件在 `chat.params` 中执行:
70
+ 内置 provider:
30
71
 
31
- 1. 仅对 `@ai-sdk/openai` provider 生效
32
- 2. 解析会话值优先级:
33
- - `OPENCODE_PROMPT_CACHE_KEY`
34
- - `OPENCODE_STICKY_SESSION_ID`
35
- - 现有 headers:`x-session-id` / `conversation_id` / `session_id`
36
- - `input.sessionID`
37
- 3. 将解析值同时写入:
38
- - `input.model.headers.x-session-id`
39
- - `input.model.headers.conversation_id`
40
- - `input.model.headers.session_id`
41
- - `output.options.promptCacheKey`
72
+ - `AnthropicCache`(`anthropic`)
73
+ - `AnthropicUltraCache`(`anthropic-ultra`)
74
+ - `FoxcodeAwsCache`(`foxcode-aws`)
42
75
 
43
- ## 可选环境变量
76
+ 自定义 provider 可使用:
44
77
 
45
- - `OPENCODE_PROMPT_CACHE_KEY`: 强制覆盖会话值(最高优先级)
46
- - `OPENCODE_STICKY_SESSION_ID`: 次优先级覆盖会话值
78
+ - `createAnthropicCachePlugin("your-provider-name")`
47
79
 
48
80
  ## 开发
49
81
 
50
82
  ```bash
83
+ npm run format
84
+ npm run lint
51
85
  npm run typecheck
52
86
  npm run build
53
87
  ```
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ export declare const CombinedPlugin: Plugin;
3
+ export default CombinedPlugin;
4
+ //# sourceMappingURL=combined-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"combined-plugin.d.ts","sourceRoot":"","sources":["../src/combined-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAgBzD,eAAO,MAAM,cAAc,EAAE,MAwE5B,CAAC;AAEF,eAAe,cAAc,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,21 +1,4 @@
1
- import type { Plugin } from "@opencode-ai/plugin";
2
- /**
3
- * set a session-scoped prompt cache key and session-scoped sticky routing headers for right.codes.
4
- *
5
- * Injects HTTP headers via `input.model.headers` so upstream proxies/load
6
- * balancers can keep a conversation pinned to one account.
7
- *
8
- * Headers injected for @ai-sdk/openai providers:
9
- * - x-session-id
10
- * - conversation_id
11
- * - session_id
12
- *
13
- * Source of truth (in order):
14
- * - env: OPENCODE_PROMPT_CACHE_KEY (manual override)
15
- * - env: OPENCODE_STICKY_SESSION_ID (manual override)
16
- * - model headers (x-session-id / conversation_id / session_id)
17
- * - opencode sessionID (default)
18
- */
19
- export declare const StickySessionPlugin: Plugin;
20
- export default StickySessionPlugin;
1
+ export { CombinedPlugin, default } from "./combined-plugin";
2
+ export { AnthropicCache, AnthropicUltraCache, createAnthropicCachePlugin, FoxcodeAwsCache, } from "./opencode-anthropic-cache";
3
+ export { StickySessionPlugin } from "./sticky-session-plugin";
21
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AA0BlD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAqCjC,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,0BAA0B,EAC1B,eAAe,GAChB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,89 @@
1
- // index.ts
1
+ // src/opencode-anthropic-cache.ts
2
+ var currentSessionId = "";
3
+ var projectId = "";
4
+ var isInitialized = false;
5
+ function buildUserId() {
6
+ const pid = projectId || "unknown";
7
+ const sid = currentSessionId || "unknown";
8
+ return `user_${pid}_account__session_${sid}`;
9
+ }
10
+ function isJsonContentType(headers) {
11
+ const ct = headers.get("content-type") || "";
12
+ return ct.includes("application/json");
13
+ }
14
+ function parseBody(body) {
15
+ if (!body) return null;
16
+ if (typeof body === "string") return body;
17
+ if (body instanceof Uint8Array) return new TextDecoder().decode(body);
18
+ return null;
19
+ }
20
+ function createCacheFetch() {
21
+ return async (url, init) => {
22
+ const headers = new Headers(init?.headers);
23
+ const method = (init?.method || "GET").toUpperCase();
24
+ if (method !== "POST" || !isJsonContentType(headers)) {
25
+ return fetch(url, init);
26
+ }
27
+ const rawBody = parseBody(init?.body);
28
+ if (!rawBody) return fetch(url, init);
29
+ let payload;
30
+ try {
31
+ payload = JSON.parse(rawBody);
32
+ } catch {
33
+ return fetch(url, init);
34
+ }
35
+ if (payload && typeof payload === "object") {
36
+ if (!payload.metadata || typeof payload.metadata !== "object") {
37
+ payload.metadata = {};
38
+ }
39
+ const meta = payload.metadata;
40
+ if (meta.user_id == null) {
41
+ meta.user_id = buildUserId();
42
+ }
43
+ }
44
+ headers.delete("content-length");
45
+ return fetch(url, { ...init, headers, body: JSON.stringify(payload) });
46
+ };
47
+ }
48
+ function createEventHandler() {
49
+ return async ({ event }) => {
50
+ if (event.type === "session.created" || event.type === "session.updated") {
51
+ const props = event.properties;
52
+ currentSessionId = props?.info?.id || "";
53
+ }
54
+ };
55
+ }
56
+ function createProviderPlugin(providerName) {
57
+ return async ({ project }) => {
58
+ if (!isInitialized) {
59
+ projectId = project?.id || "";
60
+ isInitialized = true;
61
+ }
62
+ return {
63
+ event: createEventHandler(),
64
+ auth: {
65
+ provider: providerName,
66
+ methods: [{ type: "api", label: "key" }],
67
+ loader: async () => ({
68
+ fetch: createCacheFetch()
69
+ })
70
+ }
71
+ };
72
+ };
73
+ }
74
+ var FoxcodeAwsCache = createProviderPlugin("foxcode-aws");
75
+ var AnthropicCache = createProviderPlugin("anthropic");
76
+ var AnthropicUltraCache = createProviderPlugin("anthropic-ultra");
77
+ var createAnthropicCachePlugin = createProviderPlugin;
78
+ var opencode_anthropic_cache_default = AnthropicCache;
79
+
80
+ // src/sticky-session-plugin.ts
2
81
  var OPENAI_PROVIDER_NPM = "@ai-sdk/openai";
3
- var SESSION_HEADER_KEYS = ["x-session-id", "conversation_id", "session_id"];
82
+ var SESSION_HEADER_KEYS = [
83
+ "x-session-id",
84
+ "conversation_id",
85
+ "session_id"
86
+ ];
4
87
  var normalize = (value) => typeof value === "string" ? value.trim() : "";
5
88
  var pickHeaderSessionValue = (headers) => {
6
89
  for (const key of SESSION_HEADER_KEYS) {
@@ -51,9 +134,94 @@ var StickySessionPlugin = async ({ client }) => {
51
134
  }
52
135
  };
53
136
  };
54
- var index_default = StickySessionPlugin;
137
+
138
+ // src/combined-plugin.ts
139
+ var mergeHook = (first, second) => {
140
+ if (!first) return second;
141
+ if (!second) return first;
142
+ return async (...args) => {
143
+ await first(...args);
144
+ await second(...args);
145
+ };
146
+ };
147
+ var CombinedPlugin = async (input) => {
148
+ const stickyHooks = await StickySessionPlugin(input);
149
+ const anthropicHooks = await opencode_anthropic_cache_default(input);
150
+ const merged = {
151
+ ...anthropicHooks,
152
+ ...stickyHooks,
153
+ tool: {
154
+ ...anthropicHooks.tool ?? {},
155
+ ...stickyHooks.tool ?? {}
156
+ },
157
+ "chat.message": mergeHook(
158
+ anthropicHooks["chat.message"],
159
+ stickyHooks["chat.message"]
160
+ ),
161
+ "chat.params": mergeHook(
162
+ anthropicHooks["chat.params"],
163
+ stickyHooks["chat.params"]
164
+ ),
165
+ "chat.headers": mergeHook(
166
+ anthropicHooks["chat.headers"],
167
+ stickyHooks["chat.headers"]
168
+ ),
169
+ "permission.ask": mergeHook(
170
+ anthropicHooks["permission.ask"],
171
+ stickyHooks["permission.ask"]
172
+ ),
173
+ "command.execute.before": mergeHook(
174
+ anthropicHooks["command.execute.before"],
175
+ stickyHooks["command.execute.before"]
176
+ ),
177
+ "tool.execute.before": mergeHook(
178
+ anthropicHooks["tool.execute.before"],
179
+ stickyHooks["tool.execute.before"]
180
+ ),
181
+ "shell.env": mergeHook(
182
+ anthropicHooks["shell.env"],
183
+ stickyHooks["shell.env"]
184
+ ),
185
+ "tool.execute.after": mergeHook(
186
+ anthropicHooks["tool.execute.after"],
187
+ stickyHooks["tool.execute.after"]
188
+ ),
189
+ "experimental.chat.messages.transform": mergeHook(
190
+ anthropicHooks["experimental.chat.messages.transform"],
191
+ stickyHooks["experimental.chat.messages.transform"]
192
+ ),
193
+ "experimental.chat.system.transform": mergeHook(
194
+ anthropicHooks["experimental.chat.system.transform"],
195
+ stickyHooks["experimental.chat.system.transform"]
196
+ ),
197
+ "experimental.session.compacting": mergeHook(
198
+ anthropicHooks["experimental.session.compacting"],
199
+ stickyHooks["experimental.session.compacting"]
200
+ ),
201
+ "experimental.text.complete": mergeHook(
202
+ anthropicHooks["experimental.text.complete"],
203
+ stickyHooks["experimental.text.complete"]
204
+ ),
205
+ "tool.definition": mergeHook(
206
+ anthropicHooks["tool.definition"],
207
+ stickyHooks["tool.definition"]
208
+ ),
209
+ event: mergeHook(anthropicHooks.event, stickyHooks.event),
210
+ config: mergeHook(anthropicHooks.config, stickyHooks.config)
211
+ };
212
+ if (merged.auth === void 0) {
213
+ merged.auth = anthropicHooks.auth ?? stickyHooks.auth;
214
+ }
215
+ return merged;
216
+ };
217
+ var combined_plugin_default = CombinedPlugin;
55
218
  export {
219
+ AnthropicCache,
220
+ AnthropicUltraCache,
221
+ CombinedPlugin,
222
+ FoxcodeAwsCache,
56
223
  StickySessionPlugin,
57
- index_default as default
224
+ createAnthropicCachePlugin,
225
+ combined_plugin_default as default
58
226
  };
59
227
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../index.ts"],
4
- "sourcesContent": ["import type { Plugin } from \"@opencode-ai/plugin\";\n\nconst OPENAI_PROVIDER_NPM = \"@ai-sdk/openai\";\nconst SESSION_HEADER_KEYS = [\"x-session-id\", \"conversation_id\", \"session_id\"] as const;\n\ntype HeaderMap = Record<string, unknown>;\n\nconst normalize = (value: unknown): string => (typeof value === \"string\" ? value.trim() : \"\");\n\nconst pickHeaderSessionValue = (headers: HeaderMap): string => {\n for (const key of SESSION_HEADER_KEYS) {\n const value = normalize(headers[key]);\n if (value) return value;\n }\n return \"\";\n};\n\nconst ensureHeaders = (model: { headers?: unknown }): HeaderMap => {\n if (model.headers && typeof model.headers === \"object\") {\n return model.headers as HeaderMap;\n }\n const headers: HeaderMap = {};\n model.headers = headers;\n return headers;\n};\n\n/**\n * set a session-scoped prompt cache key and session-scoped sticky routing headers for right.codes.\n *\n * Injects HTTP headers via `input.model.headers` so upstream proxies/load\n * balancers can keep a conversation pinned to one account.\n *\n * Headers injected for @ai-sdk/openai providers:\n * - x-session-id\n * - conversation_id\n * - session_id\n *\n * Source of truth (in order):\n * - env: OPENCODE_PROMPT_CACHE_KEY (manual override)\n * - env: OPENCODE_STICKY_SESSION_ID (manual override)\n * - model headers (x-session-id / conversation_id / session_id)\n * - opencode sessionID (default)\n */\nexport const StickySessionPlugin: Plugin = async ({ client }) => {\n const envPromptCacheKey = normalize(process.env.OPENCODE_PROMPT_CACHE_KEY);\n const envStickySessionID = normalize(process.env.OPENCODE_STICKY_SESSION_ID);\n const envOverride = envPromptCacheKey || envStickySessionID;\n const isProviderCodexMap: Record<string, boolean> = {};\n let inited = false;\n const init = async () => {\n if (inited) return;\n const resp = await client.config.providers();\n const providers = resp.data?.providers ?? [];\n for (const provider of providers) {\n isProviderCodexMap[provider.id] = Boolean(provider.options?.isCodex);\n }\n inited = true;\n };\n\n return {\n \"chat.params\": async (input, output) => {\n if (!inited) await init();\n const providerNpm = normalize(input.model.api.npm);\n const isCodex = isProviderCodexMap[input.model.providerID] ?? false;\n if (!providerNpm.includes(OPENAI_PROVIDER_NPM) && !isCodex) return;\n\n const headers = ensureHeaders(input.model);\n const sessionValue = envOverride || pickHeaderSessionValue(headers) || normalize(input.sessionID);\n if (!sessionValue) return;\n\n for (const key of SESSION_HEADER_KEYS) {\n headers[key] = sessionValue;\n }\n\n output.options.promptCacheKey = sessionValue;\n if (isCodex && !output.options.instructions) {\n output.options.instructions = \"\";\n }\n },\n };\n};\n\nexport default StickySessionPlugin;\n"],
5
- "mappings": ";AAEA,IAAM,sBAAsB;AAC5B,IAAM,sBAAsB,CAAC,gBAAgB,mBAAmB,YAAY;AAI5E,IAAM,YAAY,CAAC,UAA4B,OAAO,UAAU,WAAW,MAAM,KAAK,IAAI;AAE1F,IAAM,yBAAyB,CAAC,YAA+B;AAC7D,aAAW,OAAO,qBAAqB;AACrC,UAAM,QAAQ,UAAU,QAAQ,GAAG,CAAC;AACpC,QAAI,MAAO,QAAO;AAAA,EACpB;AACA,SAAO;AACT;AAEA,IAAM,gBAAgB,CAAC,UAA4C;AACjE,MAAI,MAAM,WAAW,OAAO,MAAM,YAAY,UAAU;AACtD,WAAO,MAAM;AAAA,EACf;AACA,QAAM,UAAqB,CAAC;AAC5B,QAAM,UAAU;AAChB,SAAO;AACT;AAmBO,IAAM,sBAA8B,OAAO,EAAE,OAAO,MAAM;AAC/D,QAAM,oBAAoB,UAAU,QAAQ,IAAI,yBAAyB;AACzE,QAAM,qBAAqB,UAAU,QAAQ,IAAI,0BAA0B;AAC3E,QAAM,cAAc,qBAAqB;AACzC,QAAM,qBAA8C,CAAC;AACrD,MAAI,SAAS;AACb,QAAM,OAAO,YAAY;AACvB,QAAI,OAAQ;AACZ,UAAM,OAAO,MAAM,OAAO,OAAO,UAAU;AAC3C,UAAM,YAAY,KAAK,MAAM,aAAa,CAAC;AAC3C,eAAW,YAAY,WAAW;AAChC,yBAAmB,SAAS,EAAE,IAAI,QAAQ,SAAS,SAAS,OAAO;AAAA,IACrE;AACA,aAAS;AAAA,EACX;AAEA,SAAO;AAAA,IACL,eAAe,OAAO,OAAO,WAAW;AACtC,UAAI,CAAC,OAAQ,OAAM,KAAK;AACxB,YAAM,cAAc,UAAU,MAAM,MAAM,IAAI,GAAG;AACjD,YAAM,UAAU,mBAAmB,MAAM,MAAM,UAAU,KAAK;AAC9D,UAAI,CAAC,YAAY,SAAS,mBAAmB,KAAK,CAAC,QAAS;AAE5D,YAAM,UAAU,cAAc,MAAM,KAAK;AACzC,YAAM,eAAe,eAAe,uBAAuB,OAAO,KAAK,UAAU,MAAM,SAAS;AAChG,UAAI,CAAC,aAAc;AAEnB,iBAAW,OAAO,qBAAqB;AACrC,gBAAQ,GAAG,IAAI;AAAA,MACjB;AAEA,aAAO,QAAQ,iBAAiB;AAChC,UAAI,WAAW,CAAC,OAAO,QAAQ,cAAc;AAC3C,eAAO,QAAQ,eAAe;AAAA,MAChC;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,gBAAQ;",
3
+ "sources": ["../src/opencode-anthropic-cache.ts", "../src/sticky-session-plugin.ts", "../src/combined-plugin.ts"],
4
+ "sourcesContent": ["import type { Hooks, Plugin } from \"@opencode-ai/plugin\";\n\n// Session and project tracking\nlet currentSessionId = \"\";\nlet projectId = \"\";\nlet isInitialized = false;\nfunction buildUserId() {\n const pid = projectId || \"unknown\";\n const sid = currentSessionId || \"unknown\";\n return `user_${pid}_account__session_${sid}`;\n}\nfunction isJsonContentType(headers: Headers) {\n const ct = headers.get(\"content-type\") || \"\";\n return ct.includes(\"application/json\");\n}\nfunction parseBody(body?: unknown) {\n if (!body) return null;\n if (typeof body === \"string\") return body;\n if (body instanceof Uint8Array) return new TextDecoder().decode(body);\n return null;\n}\ntype CachePayload = {\n metadata?: {\n user_id?: string;\n [key: string]: unknown;\n };\n [key: string]: unknown;\n};\n\n// Create the fetch interceptor that injects metadata.user_id\nfunction createCacheFetch() {\n return async (url: string, init?: RequestInit) => {\n const headers = new Headers(init?.headers);\n const method = (init?.method || \"GET\").toUpperCase();\n if (method !== \"POST\" || !isJsonContentType(headers)) {\n return fetch(url, init);\n }\n const rawBody = parseBody(init?.body);\n if (!rawBody) return fetch(url, init);\n let payload: CachePayload;\n try {\n payload = JSON.parse(rawBody) as CachePayload;\n } catch {\n return fetch(url, init);\n }\n if (payload && typeof payload === \"object\") {\n if (!payload.metadata || typeof payload.metadata !== \"object\") {\n payload.metadata = {};\n }\n const meta = payload.metadata;\n if (meta.user_id == null) {\n meta.user_id = buildUserId();\n }\n }\n headers.delete(\"content-length\");\n return fetch(url, { ...init, headers, body: JSON.stringify(payload) });\n };\n}\n// Shared event handler\nfunction createEventHandler(): Hooks[\"event\"] {\n return async ({ event }) => {\n if (event.type === \"session.created\" || event.type === \"session.updated\") {\n const props = event.properties;\n currentSessionId = props?.info?.id || \"\";\n }\n };\n}\n// Factory to create plugin for a specific provider\nfunction createProviderPlugin(providerName: string): Plugin {\n return async ({ project }) => {\n if (!isInitialized) {\n projectId = project?.id || \"\";\n isInitialized = true;\n }\n return {\n event: createEventHandler(),\n auth: {\n provider: providerName,\n methods: [{ type: \"api\", label: \"key\" }],\n loader: async () => ({\n fetch: createCacheFetch(),\n }),\n },\n };\n };\n}\n// Pre-built plugins for common providers\nexport const FoxcodeAwsCache = createProviderPlugin(\"foxcode-aws\");\nexport const AnthropicCache = createProviderPlugin(\"anthropic\");\nexport const AnthropicUltraCache = createProviderPlugin(\"anthropic-ultra\");\n// Generic factory for custom provider names\nexport const createAnthropicCachePlugin = createProviderPlugin;\nexport default AnthropicCache;\n", "import type { Plugin } from \"@opencode-ai/plugin\";\n\nconst OPENAI_PROVIDER_NPM = \"@ai-sdk/openai\";\nconst SESSION_HEADER_KEYS = [\n \"x-session-id\",\n \"conversation_id\",\n \"session_id\",\n] as const;\n\ntype HeaderMap = Record<string, unknown>;\n\nconst normalize = (value: unknown): string =>\n typeof value === \"string\" ? value.trim() : \"\";\n\nconst pickHeaderSessionValue = (headers: HeaderMap): string => {\n for (const key of SESSION_HEADER_KEYS) {\n const value = normalize(headers[key]);\n if (value) return value;\n }\n return \"\";\n};\n\nconst ensureHeaders = (model: { headers?: unknown }): HeaderMap => {\n if (model.headers && typeof model.headers === \"object\") {\n return model.headers as HeaderMap;\n }\n const headers: HeaderMap = {};\n model.headers = headers;\n return headers;\n};\n\n/**\n * set a session-scoped prompt cache key and session-scoped sticky routing headers for right.codes.\n *\n * Injects HTTP headers via `input.model.headers` so upstream proxies/load\n * balancers can keep a conversation pinned to one account.\n *\n * Headers injected for @ai-sdk/openai providers:\n * - x-session-id\n * - conversation_id\n * - session_id\n *\n * Source of truth (in order):\n * - env: OPENCODE_PROMPT_CACHE_KEY (manual override)\n * - env: OPENCODE_STICKY_SESSION_ID (manual override)\n * - model headers (x-session-id / conversation_id / session_id)\n * - opencode sessionID (default)\n */\nexport const StickySessionPlugin: Plugin = async ({ client }) => {\n const envPromptCacheKey = normalize(process.env.OPENCODE_PROMPT_CACHE_KEY);\n const envStickySessionID = normalize(process.env.OPENCODE_STICKY_SESSION_ID);\n const envOverride = envPromptCacheKey || envStickySessionID;\n const isProviderCodexMap: Record<string, boolean> = {};\n let inited = false;\n const init = async () => {\n if (inited) return;\n const resp = await client.config.providers();\n const providers = resp.data?.providers ?? [];\n for (const provider of providers) {\n isProviderCodexMap[provider.id] = Boolean(provider.options?.isCodex);\n }\n inited = true;\n };\n\n return {\n \"chat.params\": async (input, output) => {\n if (!inited) await init();\n const providerNpm = normalize(input.model.api.npm);\n const isCodex = isProviderCodexMap[input.model.providerID] ?? false;\n if (!providerNpm.includes(OPENAI_PROVIDER_NPM) && !isCodex) return;\n\n const headers = ensureHeaders(input.model);\n const sessionValue =\n envOverride ||\n pickHeaderSessionValue(headers) ||\n normalize(input.sessionID);\n if (!sessionValue) return;\n\n for (const key of SESSION_HEADER_KEYS) {\n headers[key] = sessionValue;\n }\n\n output.options.promptCacheKey = sessionValue;\n if (isCodex && !output.options.instructions) {\n output.options.instructions = \"\";\n }\n },\n };\n};\n\nexport default StickySessionPlugin;\n", "import type { Hooks, Plugin } from \"@opencode-ai/plugin\";\nimport AnthropicCachePlugin from \"./opencode-anthropic-cache\";\nimport { StickySessionPlugin } from \"./sticky-session-plugin\";\n\nconst mergeHook = <TArgs extends unknown[]>(\n first?: (...args: TArgs) => Promise<void>,\n second?: (...args: TArgs) => Promise<void>,\n): ((...args: TArgs) => Promise<void>) | undefined => {\n if (!first) return second;\n if (!second) return first;\n return async (...args: TArgs) => {\n await first(...args);\n await second(...args);\n };\n};\n\nexport const CombinedPlugin: Plugin = async (input) => {\n const stickyHooks = await StickySessionPlugin(input);\n const anthropicHooks = await AnthropicCachePlugin(input);\n\n const merged: Hooks = {\n ...anthropicHooks,\n ...stickyHooks,\n tool: {\n ...(anthropicHooks.tool ?? {}),\n ...(stickyHooks.tool ?? {}),\n },\n \"chat.message\": mergeHook(\n anthropicHooks[\"chat.message\"],\n stickyHooks[\"chat.message\"],\n ),\n \"chat.params\": mergeHook(\n anthropicHooks[\"chat.params\"],\n stickyHooks[\"chat.params\"],\n ),\n \"chat.headers\": mergeHook(\n anthropicHooks[\"chat.headers\"],\n stickyHooks[\"chat.headers\"],\n ),\n \"permission.ask\": mergeHook(\n anthropicHooks[\"permission.ask\"],\n stickyHooks[\"permission.ask\"],\n ),\n \"command.execute.before\": mergeHook(\n anthropicHooks[\"command.execute.before\"],\n stickyHooks[\"command.execute.before\"],\n ),\n \"tool.execute.before\": mergeHook(\n anthropicHooks[\"tool.execute.before\"],\n stickyHooks[\"tool.execute.before\"],\n ),\n \"shell.env\": mergeHook(\n anthropicHooks[\"shell.env\"],\n stickyHooks[\"shell.env\"],\n ),\n \"tool.execute.after\": mergeHook(\n anthropicHooks[\"tool.execute.after\"],\n stickyHooks[\"tool.execute.after\"],\n ),\n \"experimental.chat.messages.transform\": mergeHook(\n anthropicHooks[\"experimental.chat.messages.transform\"],\n stickyHooks[\"experimental.chat.messages.transform\"],\n ),\n \"experimental.chat.system.transform\": mergeHook(\n anthropicHooks[\"experimental.chat.system.transform\"],\n stickyHooks[\"experimental.chat.system.transform\"],\n ),\n \"experimental.session.compacting\": mergeHook(\n anthropicHooks[\"experimental.session.compacting\"],\n stickyHooks[\"experimental.session.compacting\"],\n ),\n \"experimental.text.complete\": mergeHook(\n anthropicHooks[\"experimental.text.complete\"],\n stickyHooks[\"experimental.text.complete\"],\n ),\n \"tool.definition\": mergeHook(\n anthropicHooks[\"tool.definition\"],\n stickyHooks[\"tool.definition\"],\n ),\n event: mergeHook(anthropicHooks.event, stickyHooks.event),\n config: mergeHook(anthropicHooks.config, stickyHooks.config),\n };\n\n if (merged.auth === undefined) {\n merged.auth = anthropicHooks.auth ?? stickyHooks.auth;\n }\n\n return merged;\n};\n\nexport default CombinedPlugin;\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,8 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ declare function createProviderPlugin(providerName: string): Plugin;
3
+ export declare const FoxcodeAwsCache: Plugin;
4
+ export declare const AnthropicCache: Plugin;
5
+ export declare const AnthropicUltraCache: Plugin;
6
+ export declare const createAnthropicCachePlugin: typeof createProviderPlugin;
7
+ export default AnthropicCache;
8
+ //# sourceMappingURL=opencode-anthropic-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-anthropic-cache.d.ts","sourceRoot":"","sources":["../src/opencode-anthropic-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAoEzD,iBAAS,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAiB1D;AAED,eAAO,MAAM,eAAe,QAAsC,CAAC;AACnE,eAAO,MAAM,cAAc,QAAoC,CAAC;AAChE,eAAO,MAAM,mBAAmB,QAA0C,CAAC;AAE3E,eAAO,MAAM,0BAA0B,6BAAuB,CAAC;AAC/D,eAAe,cAAc,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ /**
3
+ * set a session-scoped prompt cache key and session-scoped sticky routing headers for right.codes.
4
+ *
5
+ * Injects HTTP headers via `input.model.headers` so upstream proxies/load
6
+ * balancers can keep a conversation pinned to one account.
7
+ *
8
+ * Headers injected for @ai-sdk/openai providers:
9
+ * - x-session-id
10
+ * - conversation_id
11
+ * - session_id
12
+ *
13
+ * Source of truth (in order):
14
+ * - env: OPENCODE_PROMPT_CACHE_KEY (manual override)
15
+ * - env: OPENCODE_STICKY_SESSION_ID (manual override)
16
+ * - model headers (x-session-id / conversation_id / session_id)
17
+ * - opencode sessionID (default)
18
+ */
19
+ export declare const StickySessionPlugin: Plugin;
20
+ export default StickySessionPlugin;
21
+ //# sourceMappingURL=sticky-session-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sticky-session-plugin.d.ts","sourceRoot":"","sources":["../src/sticky-session-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AA+BlD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAwCjC,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-fixes-huihui",
3
- "version": "0.1.4-beta.1",
3
+ "version": "0.1.4-beta.2",
4
4
  "description": "Unified sticky-session plugin for opencode with session headers and prompt cache key.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -19,10 +19,13 @@
19
19
  "README.md"
20
20
  ],
21
21
  "scripts": {
22
+ "clean": "rm -rf dist",
23
+ "lint": "biome check --config-path .biome.json src package.json tsconfig.json",
24
+ "format": "biome format --write --config-path .biome.json src package.json tsconfig.json",
22
25
  "typecheck": "tsc --noEmit",
23
26
  "build:types": "tsc -p tsconfig.json --emitDeclarationOnly",
24
- "build:js": "esbuild index.ts --bundle --platform=node --target=es2022 --format=esm --outfile=dist/index.js --loader:.txt=text --sourcemap",
25
- "build": "npm run build:types && npm run build:js",
27
+ "build:js": "esbuild src/index.ts --bundle --platform=node --target=es2022 --format=esm --outfile=dist/index.js --loader:.txt=text --sourcemap",
28
+ "build": "npm run clean && npm run build:types && npm run build:js",
26
29
  "prepublishOnly": "npm run typecheck && npm run build"
27
30
  },
28
31
  "keywords": [
@@ -46,6 +49,7 @@
46
49
  "node": ">=18"
47
50
  },
48
51
  "devDependencies": {
52
+ "@biomejs/biome": "^2.4.4",
49
53
  "@opencode-ai/plugin": "^1.2.6",
50
54
  "@types/node": "^25.2.3",
51
55
  "esbuild": "^0.27.3",