@hiai-gg/hiai-opencode 0.1.5 → 0.1.7

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.
Files changed (180) hide show
  1. package/.env.example +21 -8
  2. package/AGENTS.md +60 -6
  3. package/ARCHITECTURE.md +6 -3
  4. package/LICENSE.md +0 -1
  5. package/README.md +113 -33
  6. package/assets/cli/hiai-opencode.mjs +668 -7
  7. package/assets/mcp/mempalace.mjs +159 -25
  8. package/config/hiai-opencode.schema.json +29 -3
  9. package/dist/agents/agent-skills.d.ts +7 -0
  10. package/dist/agents/bob/default.d.ts +1 -0
  11. package/dist/agents/bob/gemini.d.ts +1 -0
  12. package/dist/agents/bob/gpt-pro.d.ts +1 -0
  13. package/dist/agents/brainstormer.d.ts +7 -0
  14. package/dist/agents/coder/gpt-codex.d.ts +1 -1
  15. package/dist/agents/coder/gpt-pro.d.ts +1 -0
  16. package/dist/agents/coder/gpt.d.ts +2 -1
  17. package/dist/agents/designer.d.ts +7 -0
  18. package/dist/agents/dynamic-agent-core-sections.d.ts +4 -1
  19. package/dist/agents/dynamic-agent-prompt-builder.d.ts +1 -1
  20. package/dist/agents/strategist/gemini.d.ts +1 -0
  21. package/dist/agents/strategist/gpt.d.ts +1 -0
  22. package/dist/agents/types.d.ts +3 -1
  23. package/dist/config/index.d.ts +0 -1
  24. package/dist/config/platform-schema.d.ts +34 -6
  25. package/dist/config/schema/commands.d.ts +1 -0
  26. package/dist/config/schema/hooks.d.ts +0 -2
  27. package/dist/config/schema/index.d.ts +0 -2
  28. package/dist/config/schema/oh-my-opencode-config.d.ts +1 -9
  29. package/dist/config/types.d.ts +4 -4
  30. package/dist/create-hooks.d.ts +0 -2
  31. package/dist/features/builtin-commands/templates/doctor.d.ts +1 -0
  32. package/dist/features/builtin-commands/types.d.ts +1 -1
  33. package/dist/features/builtin-skills/skills/hiai-opencode-setup.d.ts +2 -0
  34. package/dist/features/builtin-skills/skills/index.d.ts +2 -0
  35. package/dist/features/builtin-skills/skills/website-copywriting.d.ts +2 -0
  36. package/dist/hooks/agent-usage-reminder/constants.d.ts +1 -1
  37. package/dist/hooks/index.d.ts +0 -2
  38. package/dist/hooks/keyword-detector/ultrawork/default.d.ts +1 -1
  39. package/dist/hooks/keyword-detector/ultrawork/gemini.d.ts +1 -1
  40. package/dist/hooks/keyword-detector/ultrawork/gpt.d.ts +1 -1
  41. package/dist/hooks/keyword-detector/ultrawork/planner.d.ts +1 -1
  42. package/dist/index.js +7719 -153698
  43. package/dist/mcp/index.d.ts +0 -1
  44. package/dist/mcp/registry.d.ts +1 -1
  45. package/dist/plugin/hooks/create-core-hooks.d.ts +0 -2
  46. package/dist/plugin/hooks/create-session-hooks.d.ts +1 -3
  47. package/dist/plugin/startup-diagnostics.d.ts +1 -0
  48. package/dist/shared/logger.d.ts +2 -0
  49. package/dist/shared/mcp-static-export.d.ts +22 -0
  50. package/dist/shared/mode-routing.d.ts +6 -0
  51. package/dist/tools/ast-grep/constants.d.ts +1 -1
  52. package/dist/tools/ast-grep/environment-check.d.ts +1 -5
  53. package/dist/tools/ast-grep/language-support.d.ts +0 -1
  54. package/dist/tools/ast-grep/types.d.ts +1 -2
  55. package/dist/tools/delegate-task/git-categories.d.ts +2 -0
  56. package/dist/tools/delegate-task/sub-agent.d.ts +2 -0
  57. package/dist/tools/skill-mcp/constants.d.ts +1 -1
  58. package/hiai-opencode.json +50 -19
  59. package/package.json +10 -5
  60. package/src/agents/agent-skills.ts +70 -0
  61. package/src/agents/bob/default.ts +7 -1
  62. package/src/agents/bob/gemini.ts +1 -0
  63. package/src/agents/bob/gpt-pro.ts +3 -1
  64. package/src/agents/bob.ts +3 -0
  65. package/src/agents/brainstormer.ts +72 -0
  66. package/src/agents/builtin-agents.ts +59 -3
  67. package/src/agents/coder/gpt-codex.ts +5 -3
  68. package/src/agents/coder/gpt-pro.ts +4 -2
  69. package/src/agents/coder/gpt.ts +3 -1
  70. package/src/agents/critic/agent.ts +1 -0
  71. package/src/agents/designer.ts +70 -0
  72. package/src/agents/dynamic-agent-category-skills-guide.ts +6 -0
  73. package/src/agents/dynamic-agent-core-sections.ts +36 -0
  74. package/src/agents/dynamic-agent-prompt-builder.ts +1 -0
  75. package/src/agents/guard/default.ts +1 -0
  76. package/src/agents/guard/gemini.ts +1 -0
  77. package/src/agents/guard/gpt.ts +1 -0
  78. package/src/agents/platform-manager.ts +17 -1
  79. package/src/agents/prompt-library/platform.ts +34 -0
  80. package/src/agents/researcher.ts +1 -0
  81. package/src/agents/strategist/gemini.ts +1 -0
  82. package/src/agents/strategist/gpt.ts +1 -0
  83. package/src/agents/types.ts +4 -1
  84. package/src/agents/ui.ts +1 -0
  85. package/src/config/defaults.ts +45 -13
  86. package/src/config/index.ts +0 -1
  87. package/src/config/model-slots-and-export.test.ts +73 -0
  88. package/src/config/platform-schema.ts +3 -3
  89. package/src/config/schema/commands.ts +1 -0
  90. package/src/config/schema/hooks.ts +0 -2
  91. package/src/config/schema/index.ts +0 -2
  92. package/src/config/schema/oh-my-opencode-config.ts +0 -5
  93. package/src/config/types.ts +4 -4
  94. package/src/features/builtin-commands/commands.ts +7 -0
  95. package/src/features/builtin-commands/templates/doctor.ts +43 -0
  96. package/src/features/builtin-commands/types.ts +1 -1
  97. package/src/features/builtin-skills/skills/hiai-opencode-setup.ts +69 -0
  98. package/src/features/builtin-skills/skills/index.ts +2 -0
  99. package/src/features/builtin-skills/skills/website-copywriting.ts +41 -0
  100. package/src/features/builtin-skills/skills.test.ts +8 -0
  101. package/src/features/builtin-skills/skills.ts +12 -1
  102. package/src/features/skill-mcp-manager/AGENTS.md +1 -1
  103. package/src/hooks/agent-usage-reminder/constants.ts +4 -4
  104. package/src/hooks/index.ts +0 -2
  105. package/src/hooks/keyword-detector/ultrawork/default.ts +18 -18
  106. package/src/hooks/keyword-detector/ultrawork/gemini.ts +21 -21
  107. package/src/hooks/keyword-detector/ultrawork/gpt.ts +6 -8
  108. package/src/hooks/keyword-detector/ultrawork/planner.ts +5 -5
  109. package/src/index.ts +8 -78
  110. package/src/internals/plugins/subtask2/commands/manifest.ts +2 -6
  111. package/src/internals/plugins/subtask2/hooks/command-hooks.ts +2 -2
  112. package/src/internals/plugins/subtask2/hooks/message-hooks.ts +1 -1
  113. package/src/internals/plugins/subtask2/parsing/parallel.ts +13 -10
  114. package/src/mcp/index.ts +0 -1
  115. package/src/mcp/registry.ts +27 -0
  116. package/src/plugin/chat-message.ts +0 -2
  117. package/src/plugin/hooks/create-session-hooks.ts +0 -17
  118. package/src/plugin/startup-diagnostics.ts +27 -0
  119. package/src/plugin-handlers/agent-config-handler.ts +3 -2
  120. package/src/plugin-handlers/mcp-config-handler.test.ts +63 -0
  121. package/src/plugin-handlers/mcp-config-handler.ts +29 -14
  122. package/src/plugin-handlers/strategist-agent-config-builder.ts +1 -1
  123. package/src/shared/agent-display-names.test.ts +9 -0
  124. package/src/shared/agent-display-names.ts +5 -0
  125. package/src/shared/log-legacy-plugin-startup-warning.ts +6 -8
  126. package/src/shared/logger.ts +8 -0
  127. package/src/shared/mcp-static-export.ts +119 -0
  128. package/src/shared/migration/agent-names.ts +8 -0
  129. package/src/shared/migration/hook-names.ts +1 -1
  130. package/src/shared/mode-routing.test.ts +88 -0
  131. package/src/shared/mode-routing.ts +30 -0
  132. package/src/shared/startup-diagnostics.ts +6 -7
  133. package/src/tools/ast-grep/constants.ts +1 -1
  134. package/src/tools/ast-grep/environment-check.ts +2 -32
  135. package/src/tools/ast-grep/language-support.ts +0 -3
  136. package/src/tools/ast-grep/types.ts +1 -2
  137. package/src/tools/call-omo-agent/tools.ts +11 -4
  138. package/src/tools/delegate-task/anthropic-categories.ts +3 -3
  139. package/src/tools/delegate-task/builtin-categories.ts +2 -0
  140. package/src/tools/delegate-task/categories.test.ts +87 -0
  141. package/src/tools/delegate-task/category-resolver.ts +8 -9
  142. package/src/tools/delegate-task/git-categories.ts +30 -0
  143. package/src/tools/delegate-task/model-string-parser.test.ts +90 -0
  144. package/src/tools/delegate-task/openai-categories.ts +26 -22
  145. package/src/tools/delegate-task/sub-agent.ts +10 -0
  146. package/src/tools/delegate-task/subagent-discovery.test.ts +123 -0
  147. package/src/tools/delegate-task/subagent-resolver.ts +18 -1
  148. package/src/tools/skill-mcp/constants.ts +1 -1
  149. package/src/tools/skill-mcp/tools.test.ts +44 -0
  150. package/dist/ast-grep-napi.win32-x64-msvc-67c0y8nc.node +0 -0
  151. package/dist/config/loader.test.d.ts +0 -1
  152. package/dist/config/models.d.ts +0 -13
  153. package/dist/config/schema/websearch.d.ts +0 -13
  154. package/dist/hooks/no-bob-gpt/hook.d.ts +0 -16
  155. package/dist/hooks/no-bob-gpt/index.d.ts +0 -1
  156. package/dist/hooks/no-coder-non-gpt/hook.d.ts +0 -20
  157. package/dist/hooks/no-coder-non-gpt/index.d.ts +0 -1
  158. package/dist/internals/plugins/websearch-cited/google.d.ts +0 -38
  159. package/dist/internals/plugins/websearch-cited/index.d.ts +0 -17
  160. package/dist/internals/plugins/websearch-cited/openai.d.ts +0 -9
  161. package/dist/internals/plugins/websearch-cited/openrouter.d.ts +0 -2
  162. package/dist/internals/plugins/websearch-cited/types.d.ts +0 -5
  163. package/dist/mcp/grep-app.d.ts +0 -6
  164. package/dist/mcp/omo-mcp-index.d.ts +0 -10
  165. package/dist/mcp/websearch.d.ts +0 -11
  166. package/src/config/schema/websearch.ts +0 -15
  167. package/src/hooks/no-bob-gpt/hook.ts +0 -56
  168. package/src/hooks/no-bob-gpt/index.ts +0 -1
  169. package/src/hooks/no-coder-non-gpt/hook.ts +0 -67
  170. package/src/hooks/no-coder-non-gpt/index.ts +0 -1
  171. package/src/internals/plugins/websearch-cited/LICENSE +0 -214
  172. package/src/internals/plugins/websearch-cited/codex_prompt.txt +0 -79
  173. package/src/internals/plugins/websearch-cited/google.ts +0 -749
  174. package/src/internals/plugins/websearch-cited/index.ts +0 -306
  175. package/src/internals/plugins/websearch-cited/openai.ts +0 -407
  176. package/src/internals/plugins/websearch-cited/openrouter.ts +0 -190
  177. package/src/internals/plugins/websearch-cited/types.ts +0 -7
  178. package/src/mcp/grep-app.ts +0 -6
  179. package/src/mcp/omo-mcp-index.ts +0 -30
  180. package/src/mcp/websearch.ts +0 -44
@@ -1,306 +0,0 @@
1
- import { type Plugin, tool } from "@opencode-ai/plugin";
2
- import type { Config } from "@opencode-ai/sdk";
3
-
4
- import { createGoogleWebsearchClient } from "./google";
5
- import { createOpenAIWebsearchClient, type OpenAIWebsearchConfig } from "./openai";
6
- import { createOpenRouterWebsearchClient } from "./openrouter";
7
- import type { GetAuth } from "./types";
8
-
9
- export const GOOGLE_PROVIDER_ID = "google";
10
- export const OPENAI_PROVIDER_ID = "openai";
11
- export const OPENROUTER_PROVIDER_ID = "openrouter";
12
-
13
- const CITED_SEARCH_TOOL_DESCRIPTION =
14
- "Performs a Gemini-style grounded web search: returns a concise digest with inline citations and a Sources list of URLs. NOTE: for LLM rate limits, DO NOT parallel this tool > 5";
15
-
16
- const WEBSEARCH_ARGS = {
17
- query: tool.schema.string().describe("The natural language web search query."),
18
- } as const;
19
-
20
- const WEBSEARCH_ALLOWED_KEYS = new Set(Object.keys(WEBSEARCH_ARGS));
21
-
22
- const WEBSEARCH_ALLOWED_KEYS_DESCRIPTION = Array.from(WEBSEARCH_ALLOWED_KEYS)
23
- .map((key) => `'${key}'`)
24
- .join(", ");
25
-
26
- type SelectedProviderID = typeof GOOGLE_PROVIDER_ID | typeof OPENAI_PROVIDER_ID | typeof OPENROUTER_PROVIDER_ID;
27
-
28
- function isRecord(value: unknown): value is Record<string, unknown> {
29
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
30
- }
31
-
32
- const authRegistry = new Map<string, GetAuth>();
33
-
34
- export function registerGetAuth(providerID: string, getAuth: GetAuth): void {
35
- authRegistry.set(providerID, getAuth);
36
- }
37
-
38
- export function resolveGetAuth(providerID: string): GetAuth | undefined {
39
- return authRegistry.get(providerID);
40
- }
41
-
42
- type SelectedWebsearchConfig = {
43
- providerID: SelectedProviderID;
44
- model: string;
45
- };
46
-
47
- export type WebsearchCitedFallback = SelectedWebsearchConfig;
48
-
49
- type WebsearchCitedSelection = {
50
- selected?: SelectedWebsearchConfig;
51
- error?: string;
52
- };
53
-
54
- function findFirstWebsearchCitedConfig(config: Config): WebsearchCitedSelection {
55
- const providers = config.provider;
56
- if (!providers || typeof providers !== "object") {
57
- return {};
58
- }
59
-
60
- let firstError: string | undefined;
61
-
62
- for (const [providerID, providerConfig] of Object.entries(providers)) {
63
- if (!providerConfig || typeof providerConfig !== "object") {
64
- continue;
65
- }
66
-
67
- const options = (providerConfig as { options?: unknown }).options;
68
- if (!isRecord(options)) {
69
- continue;
70
- }
71
-
72
- if (!("websearch_cited" in options)) {
73
- continue;
74
- }
75
-
76
- const cited = options.websearch_cited;
77
- if (!isRecord(cited)) {
78
- firstError ??= `Invalid websearch_cited configuration for provider "${providerID}".`;
79
- continue;
80
- }
81
-
82
- const candidate = cited.model;
83
- if (typeof candidate !== "string" || candidate.trim() === "") {
84
- firstError ??= `Missing websearch_cited model for provider "${providerID}".`;
85
- continue;
86
- }
87
-
88
- if (
89
- providerID !== GOOGLE_PROVIDER_ID &&
90
- providerID !== OPENAI_PROVIDER_ID &&
91
- providerID !== OPENROUTER_PROVIDER_ID
92
- ) {
93
- firstError ??= `Unsupported provider "${providerID}" for websearch_cited.`;
94
- continue;
95
- }
96
-
97
- return {
98
- selected: {
99
- providerID: providerID as SelectedProviderID,
100
- model: candidate.trim(),
101
- },
102
- };
103
- }
104
-
105
- return firstError ? { error: firstError } : {};
106
- }
107
-
108
- function parseOpenAIOptions(providerConfig: unknown, model: string | undefined): OpenAIWebsearchConfig {
109
- if (!isRecord(providerConfig)) {
110
- return {};
111
- }
112
-
113
- const providerRecord = providerConfig;
114
-
115
- const rawOptions = (providerRecord as { options?: unknown }).options;
116
- const baseOptions = isRecord(rawOptions) ? rawOptions : undefined;
117
-
118
- let modelOptions: Record<string, unknown> | undefined;
119
- const rawModels = (providerRecord as { models?: unknown }).models;
120
- if (model && isRecord(rawModels)) {
121
- const modelsRecord: Record<string, unknown> = rawModels;
122
- const entry = modelsRecord[model];
123
- if (isRecord(entry)) {
124
- const entryOptions = (entry as { options?: unknown }).options;
125
- if (isRecord(entryOptions)) {
126
- modelOptions = entryOptions;
127
- }
128
- }
129
- }
130
-
131
- const merged: Record<string, unknown> = {
132
- ...(baseOptions ?? {}),
133
- ...(modelOptions ?? {}),
134
- };
135
-
136
- const result: OpenAIWebsearchConfig = {};
137
-
138
- const reasoningEffort = merged.reasoningEffort;
139
- if (typeof reasoningEffort === "string" && reasoningEffort.trim() !== "") {
140
- result.reasoningEffort = reasoningEffort.trim();
141
- }
142
-
143
- const reasoningSummary = merged.reasoningSummary;
144
- if (typeof reasoningSummary === "string" && reasoningSummary.trim() !== "") {
145
- result.reasoningSummary = reasoningSummary.trim();
146
- }
147
-
148
- const textVerbosity = merged.textVerbosity;
149
- if (typeof textVerbosity === "string" && textVerbosity.trim() !== "") {
150
- result.textVerbosity = textVerbosity.trim();
151
- }
152
-
153
- const store = merged.store;
154
- if (typeof store === "boolean") {
155
- result.store = store;
156
- }
157
-
158
- const include = merged.include;
159
- if (Array.isArray(include)) {
160
- const filtered = include.filter((value) => typeof value === "string" && value.trim() !== "");
161
- if (filtered.length > 0) {
162
- result.include = filtered;
163
- }
164
- }
165
-
166
- return result;
167
- }
168
-
169
- const WebsearchCitedPlugin = (_ctx?: unknown, fallback?: WebsearchCitedFallback): Promise<any> => {
170
- let selectedProvider: SelectedProviderID | undefined = fallback?.providerID;
171
- let selectedModel: string | undefined = fallback?.model;
172
- let openaiConfig: OpenAIWebsearchConfig = {};
173
- let configError: string | undefined;
174
-
175
- return Promise.resolve({
176
- auth: {
177
- provider: OPENROUTER_PROVIDER_ID,
178
- loader(getAuth: GetAuth) {
179
- registerGetAuth(OPENROUTER_PROVIDER_ID, getAuth);
180
- return Promise.resolve({});
181
- },
182
- methods: [
183
- {
184
- type: "api",
185
- label: "OpenRouter API key",
186
- },
187
- ],
188
- },
189
- config: (config: Config) => {
190
- const { selected, error } = findFirstWebsearchCitedConfig(config as any);
191
-
192
- selectedProvider = undefined;
193
- selectedModel = undefined;
194
- openaiConfig = {};
195
- configError = error;
196
-
197
- if (selected) {
198
- selectedProvider = selected.providerID;
199
- selectedModel = selected.model;
200
- if (selectedProvider === OPENAI_PROVIDER_ID) {
201
- const openaiProvider = config.provider?.openai;
202
- openaiConfig = parseOpenAIOptions(openaiProvider, selectedModel);
203
- }
204
- } else if (!error && fallback) {
205
- selectedProvider = fallback.providerID;
206
- selectedModel = fallback.model;
207
- }
208
-
209
- return Promise.resolve();
210
- },
211
- tool: {
212
- websearch_cited: tool({
213
- description: CITED_SEARCH_TOOL_DESCRIPTION,
214
- args: WEBSEARCH_ARGS,
215
- async execute(args, context) {
216
- const argKeys = Object.keys(args ?? {});
217
- const extraKeys = argKeys.filter((key) => !WEBSEARCH_ALLOWED_KEYS.has(key));
218
- if (extraKeys.length > 0) {
219
- throw new Error(
220
- `Unknown argument(s): ${extraKeys.join(", ")}, only ${WEBSEARCH_ALLOWED_KEYS_DESCRIPTION} supported.`
221
- );
222
- }
223
-
224
- const query = args.query?.trim();
225
- if (!query) {
226
- throw new Error("The 'query' parameter cannot be empty.");
227
- }
228
-
229
- if (configError) {
230
- throw new Error(configError);
231
- }
232
-
233
- if (!selectedProvider || !selectedModel) {
234
- throw new Error("Missing web search model configuration.");
235
- }
236
-
237
- if (selectedProvider === OPENAI_PROVIDER_ID) {
238
- const getAuth = resolveGetAuth(OPENAI_PROVIDER_ID);
239
- if (!getAuth) {
240
- throw new Error('Missing auth for provider "openai". Authenticate via `opencode auth login`.');
241
- }
242
-
243
- const client = createOpenAIWebsearchClient(selectedModel, openaiConfig);
244
- return client.search(query, context.abort, getAuth);
245
- }
246
-
247
- if (selectedProvider === OPENROUTER_PROVIDER_ID) {
248
- const getAuth = resolveGetAuth(OPENROUTER_PROVIDER_ID);
249
- if (!getAuth) {
250
- throw new Error('Missing auth for provider "openrouter". Authenticate via `opencode auth login`.');
251
- }
252
-
253
- const client = createOpenRouterWebsearchClient(selectedModel);
254
- return client.search(query, context.abort, getAuth);
255
- }
256
-
257
- const getAuth = resolveGetAuth(GOOGLE_PROVIDER_ID);
258
- if (!getAuth) {
259
- throw new Error('Missing auth for provider "google". Authenticate via `opencode auth login`.');
260
- }
261
-
262
- const client = createGoogleWebsearchClient(selectedModel);
263
- return client.search(query, context.abort, getAuth);
264
- },
265
- }),
266
- },
267
- });
268
- };
269
-
270
- export const WebsearchCitedGooglePlugin: Plugin = () => {
271
- return Promise.resolve({
272
- auth: {
273
- provider: GOOGLE_PROVIDER_ID,
274
- loader(getAuth) {
275
- registerGetAuth(GOOGLE_PROVIDER_ID, getAuth);
276
- return Promise.resolve({});
277
- },
278
- methods: [
279
- {
280
- type: "api",
281
- label: "Google API key",
282
- },
283
- ],
284
- },
285
- });
286
- };
287
-
288
- export const WebsearchCitedOpenAIPlugin: Plugin = () => {
289
- return Promise.resolve({
290
- auth: {
291
- provider: OPENAI_PROVIDER_ID,
292
- loader(getAuth) {
293
- registerGetAuth(OPENAI_PROVIDER_ID, getAuth);
294
- return Promise.resolve({});
295
- },
296
- methods: [
297
- {
298
- type: "api",
299
- label: "OpenAI API key",
300
- },
301
- ],
302
- },
303
- });
304
- };
305
-
306
- export default WebsearchCitedPlugin;
@@ -1,407 +0,0 @@
1
- import type { Auth as ProviderAuth } from "@opencode-ai/sdk";
2
- import codexPrompt from "./codex_prompt.txt" with { type: "text" };
3
- import type { GetAuth, WebsearchClient } from "./types.ts";
4
-
5
- type OpenAIReasoningConfig = {
6
- effort?: string;
7
- summary?: string;
8
- };
9
-
10
- type OpenAITextConfig = {
11
- verbosity?: string;
12
- };
13
-
14
- type OpenAIInputContent = {
15
- type: "input_text";
16
- text: string;
17
- };
18
-
19
- type OpenAIInputMessage = {
20
- role: "user";
21
- content: OpenAIInputContent[];
22
- };
23
-
24
- type OpenAITool = {
25
- type: "web_search";
26
- };
27
-
28
- type OpenAIResponsesRequest = {
29
- model: string;
30
- instructions: string;
31
- input: OpenAIInputMessage[];
32
- tools: OpenAITool[];
33
- reasoning?: OpenAIReasoningConfig;
34
- text?: OpenAITextConfig;
35
- store?: boolean;
36
- include?: string[];
37
- stream?: boolean;
38
- tool_choice?: string;
39
- parallel_tool_calls?: boolean;
40
- };
41
-
42
- function buildWebSearchUserPrompt(query: string): string {
43
- const normalized = query.trim();
44
- return `perform web search on "${normalized}". Return results with inline citations (**only** source index like [1], no URL in the answer) and end with a Sources list of URLs.`;
45
- }
46
-
47
- type OpenAIWebSearchOptions = {
48
- model: string;
49
- query: string;
50
- abortSignal: AbortSignal;
51
- auth: ProviderAuth;
52
- reasoningEffort?: string;
53
- reasoningSummary?: string;
54
- textVerbosity?: string;
55
- store?: boolean;
56
- include?: string[];
57
- };
58
-
59
- export type OpenAIWebsearchConfig = {
60
- reasoningEffort?: string;
61
- reasoningSummary?: string;
62
- textVerbosity?: string;
63
- store?: boolean;
64
- include?: string[];
65
- };
66
-
67
- function getAccessToken(auth: ProviderAuth): string {
68
- if (auth.type === "oauth") {
69
- const access = auth.access.trim();
70
- if (!access) {
71
- throw new Error("Missing OpenAI OAuth access token");
72
- }
73
- return access;
74
- }
75
-
76
- if (auth.type === "api") {
77
- const key = auth.key.trim();
78
- if (!key) {
79
- throw new Error("Missing OpenAI API key");
80
- }
81
- return key;
82
- }
83
-
84
- const token = auth.token.trim();
85
- if (!token) {
86
- throw new Error("Missing OpenAI token");
87
- }
88
- return token;
89
- }
90
-
91
- function extractChatGPTAccountId(auth: ProviderAuth): string | undefined {
92
- if (auth.type !== "oauth") {
93
- return undefined;
94
- }
95
-
96
- const access = auth.access.trim();
97
- if (!access) {
98
- return undefined;
99
- }
100
-
101
- const parts = access.split(".");
102
- if (parts.length !== 3) {
103
- return undefined;
104
- }
105
-
106
- try {
107
- const payload = parts[1];
108
- if (!payload) {
109
- return undefined;
110
- }
111
- const decoded = Buffer.from(payload, "base64").toString("utf8");
112
- const parsed = JSON.parse(decoded) as unknown;
113
- if (!parsed || typeof parsed !== "object") {
114
- return undefined;
115
- }
116
- const root = parsed as { [key: string]: unknown };
117
- const claim = root["https://api.openai.com/auth"];
118
- if (!claim || typeof claim !== "object") {
119
- return undefined;
120
- }
121
- const accountId = (claim as { chatgpt_account_id?: unknown }).chatgpt_account_id;
122
- if (typeof accountId !== "string") {
123
- return undefined;
124
- }
125
- const trimmed = accountId.trim();
126
- return trimmed === "" ? undefined : trimmed;
127
- } catch {
128
- return undefined;
129
- }
130
- }
131
-
132
- async function runOpenAIWebSearch(options: OpenAIWebSearchOptions): Promise<string> {
133
- const normalizedModel = options.model.trim();
134
- if (!normalizedModel) {
135
- throw new Error("Invalid OpenAI web search model");
136
- }
137
-
138
- const normalizedQuery = options.query.trim();
139
- if (!normalizedQuery) {
140
- throw new Error("Query must not be empty");
141
- }
142
-
143
- const accessToken = getAccessToken(options.auth);
144
- const isOAuth = options.auth.type === "oauth";
145
-
146
- const body: OpenAIResponsesRequest = {
147
- model: normalizedModel,
148
- instructions: "",
149
- input: [
150
- {
151
- role: "user",
152
- content: [
153
- {
154
- type: "input_text",
155
- text: buildWebSearchUserPrompt(normalizedQuery),
156
- },
157
- ],
158
- },
159
- ],
160
- tools: [{ type: "web_search" }],
161
- include: ["web_search_call.action.sources"],
162
- };
163
-
164
- if (options.reasoningEffort || options.reasoningSummary) {
165
- body.reasoning = {
166
- effort: options.reasoningEffort,
167
- summary: options.reasoningSummary,
168
- };
169
- }
170
- body.store = false;
171
-
172
- if (options.textVerbosity) {
173
- body.text = {
174
- verbosity: options.textVerbosity,
175
- };
176
- }
177
-
178
- if (Array.isArray(options.include) && options.include.length > 0) {
179
- const filtered = options.include.filter((value) => typeof value === "string" && value.trim() !== "");
180
- if (filtered.length > 0) {
181
- body.include = filtered;
182
- }
183
- }
184
-
185
- body.stream = true;
186
- body.tool_choice = "auto";
187
- body.parallel_tool_calls = true;
188
-
189
- if (isOAuth) {
190
- // NOTE: Do not modify Codex backend instructions; invalid instructions will be rejected.
191
- body.instructions = codexPrompt;
192
- } else {
193
- body.instructions = "You are an AI assistant answering a single web search query for the user.";
194
- }
195
-
196
- const url = isOAuth ? "https://chatgpt.com/backend-api/codex/responses" : "https://api.openai.com/v1/responses";
197
-
198
- const headers: Record<string, string> = {
199
- Authorization: `Bearer ${accessToken}`,
200
- "Content-Type": "application/json",
201
- "OpenAI-Beta": "responses=experimental",
202
- };
203
-
204
- if (isOAuth) {
205
- const accountId = extractChatGPTAccountId(options.auth);
206
- if (accountId) {
207
- headers["chatgpt-account-id"] = accountId;
208
- }
209
- headers.originator = "codex_cli_rs";
210
- }
211
-
212
- const response = await fetch(url, {
213
- method: "POST",
214
- headers,
215
- body: JSON.stringify(body),
216
- signal: options.abortSignal,
217
- });
218
-
219
- if (!response.ok) {
220
- const message = await buildErrorDetails(response, url, body);
221
- throw new Error(message);
222
- }
223
-
224
- const payload = await readOpenAIResponsePayload(response);
225
- const text = extractOpenAIText(payload);
226
-
227
- if (!text || !text.trim()) {
228
- return `Web search completed for "${normalizedQuery}", but no results were returned.`;
229
- }
230
-
231
- return text;
232
- }
233
-
234
- export function createOpenAIWebsearchClient(model: string, config: OpenAIWebsearchConfig): WebsearchClient {
235
- const normalizedModel = model.trim();
236
- if (!normalizedModel) {
237
- throw new Error("Invalid OpenAI web search model");
238
- }
239
-
240
- return {
241
- async search(query, abortSignal, getAuth: GetAuth) {
242
- const normalizedQuery = query.trim();
243
- if (!normalizedQuery) {
244
- throw new Error("Query must not be empty");
245
- }
246
-
247
- const auth = await getAuth();
248
- if (!auth) {
249
- throw new Error('Missing auth for provider "openai"');
250
- }
251
-
252
- return runOpenAIWebSearch({
253
- model: normalizedModel,
254
- query: normalizedQuery,
255
- abortSignal,
256
- auth,
257
- reasoningEffort: config.reasoningEffort,
258
- reasoningSummary: config.reasoningSummary,
259
- textVerbosity: config.textVerbosity,
260
- store: config.store,
261
- include: config.include,
262
- });
263
- },
264
- };
265
- }
266
-
267
- function extractOpenAIText(payload: unknown): string | undefined {
268
- if (!payload || typeof payload !== "object") {
269
- return undefined;
270
- }
271
-
272
- const root = payload as { output?: unknown };
273
- const output = root.output;
274
- if (!Array.isArray(output) || output.length === 0) {
275
- return undefined;
276
- }
277
-
278
- let combined = "";
279
-
280
- for (const item of output) {
281
- if (!item || typeof item !== "object") {
282
- continue;
283
- }
284
-
285
- const content = (item as { content?: unknown }).content;
286
- if (!Array.isArray(content)) {
287
- continue;
288
- }
289
-
290
- for (const part of content) {
291
- if (!part || typeof part !== "object") {
292
- continue;
293
- }
294
-
295
- const kind = (part as { type?: unknown }).type;
296
- if (kind !== "output_text") {
297
- continue;
298
- }
299
-
300
- const textField = (part as { text?: unknown }).text;
301
- if (typeof textField === "string") {
302
- combined += textField;
303
- } else if (textField && typeof textField === "object") {
304
- const obj = textField as { value?: unknown };
305
- if (typeof obj.value === "string") {
306
- combined += obj.value;
307
- }
308
- }
309
- }
310
- }
311
-
312
- return combined || undefined;
313
- }
314
-
315
- async function buildErrorDetails(response: Response, url: string, body: OpenAIResponsesRequest): Promise<string> {
316
- const parts: string[] = [];
317
- parts.push(`status=${response.status}`);
318
- parts.push(`url=${url}`);
319
- const safeBody: OpenAIResponsesRequest = { ...body };
320
- if (typeof safeBody.instructions === "string") {
321
- const value = safeBody.instructions;
322
- const maxLength = 512;
323
- if (value.length > maxLength) {
324
- const headLength = 256;
325
- const tailLength = 128;
326
- const head = value.slice(0, headLength);
327
- const tail = value.slice(-tailLength);
328
- const omitted = value.length - headLength - tailLength;
329
- safeBody.instructions = `${head} ... [${omitted} chars truncated] ... ${tail}`;
330
- }
331
- }
332
- parts.push(`requestBody=${JSON.stringify(safeBody)}`);
333
-
334
- let rawText: string | undefined;
335
- try {
336
- rawText = await response.text();
337
- } catch {}
338
-
339
- if (rawText) {
340
- let parsedMessage: string | undefined;
341
- try {
342
- const parsed = JSON.parse(rawText) as { error?: { message?: unknown } };
343
- const message = parsed.error?.message;
344
- if (typeof message === "string" && message.trim() !== "") {
345
- parsedMessage = message.trim();
346
- }
347
- } catch {}
348
-
349
- if (parsedMessage) {
350
- parts.unshift(`error=${parsedMessage}`);
351
- }
352
-
353
- parts.push(`responseBody=${rawText}`);
354
- }
355
-
356
- return parts.join(" | ");
357
- }
358
-
359
- type OpenAISseEvent = {
360
- type?: string;
361
- response?: object;
362
- };
363
-
364
- async function readOpenAIResponsePayload(response: Response): Promise<unknown> {
365
- const text = await response.text();
366
- const trimmed = text.trim();
367
- if (trimmed === "") {
368
- return {};
369
- }
370
-
371
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
372
- try {
373
- const parsed = JSON.parse(trimmed) as unknown;
374
- return parsed;
375
- } catch {}
376
- }
377
-
378
- const extracted = extractOpenAIResponseFromSse(text);
379
- if (extracted !== undefined) {
380
- return extracted;
381
- }
382
-
383
- throw new Error("Failed to parse JSON");
384
- }
385
-
386
- function extractOpenAIResponseFromSse(sseText: string): object | undefined {
387
- const lines = sseText.split("\n");
388
-
389
- for (const line of lines) {
390
- if (!line.startsWith("data: ")) {
391
- continue;
392
- }
393
- const payload = line.slice(6).trim();
394
- if (!payload || payload === "[DONE]") {
395
- continue;
396
- }
397
- try {
398
- const parsed = JSON.parse(payload) as OpenAISseEvent;
399
- const kind = parsed.type ?? "";
400
- if (kind === "response.done" || kind === "response.completed") {
401
- return parsed.response;
402
- }
403
- } catch {}
404
- }
405
-
406
- return undefined;
407
- }