@corbat-tech/coco 2.11.0 → 2.12.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/dist/index.js CHANGED
@@ -189,7 +189,7 @@ function getDefaultModel(provider) {
189
189
  case "anthropic":
190
190
  return process.env["ANTHROPIC_MODEL"] ?? "claude-opus-4-6";
191
191
  case "openai":
192
- return process.env["OPENAI_MODEL"] ?? "gpt-5.3-codex";
192
+ return process.env["OPENAI_MODEL"] ?? "gpt-5.4-codex";
193
193
  case "gemini":
194
194
  return process.env["GEMINI_MODEL"] ?? "gemini-3.1-pro-preview";
195
195
  case "kimi":
@@ -201,7 +201,7 @@ function getDefaultModel(provider) {
201
201
  case "ollama":
202
202
  return process.env["OLLAMA_MODEL"] ?? "llama3.1";
203
203
  case "codex":
204
- return process.env["CODEX_MODEL"] ?? "gpt-5.3-codex";
204
+ return process.env["CODEX_MODEL"] ?? "gpt-5.4-codex";
205
205
  case "copilot":
206
206
  return process.env["COPILOT_MODEL"] ?? "claude-sonnet-4.6";
207
207
  case "groq":
@@ -219,7 +219,7 @@ function getDefaultModel(provider) {
219
219
  case "qwen":
220
220
  return process.env["QWEN_MODEL"] ?? "qwen-coder-plus";
221
221
  default:
222
- return "gpt-5.3-codex";
222
+ return "gpt-5.4-codex";
223
223
  }
224
224
  }
225
225
  function getDefaultProvider() {
@@ -11805,12 +11805,16 @@ var AnthropicProvider = class {
11805
11805
  );
11806
11806
  const streamTimeout = this.config.timeout ?? 12e4;
11807
11807
  let lastActivityTime = Date.now();
11808
- const checkTimeout = () => {
11808
+ const timeoutController = new AbortController();
11809
+ const timeoutInterval = setInterval(() => {
11809
11810
  if (Date.now() - lastActivityTime > streamTimeout) {
11810
- throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
11811
+ clearInterval(timeoutInterval);
11812
+ timeoutController.abort();
11811
11813
  }
11812
- };
11813
- const timeoutInterval = setInterval(checkTimeout, 5e3);
11814
+ }, 5e3);
11815
+ timeoutController.signal.addEventListener("abort", () => stream.controller.abort(), {
11816
+ once: true
11817
+ });
11814
11818
  try {
11815
11819
  let streamStopReason;
11816
11820
  for await (const event of stream) {
@@ -11831,6 +11835,9 @@ var AnthropicProvider = class {
11831
11835
  } finally {
11832
11836
  clearInterval(timeoutInterval);
11833
11837
  }
11838
+ if (timeoutController.signal.aborted) {
11839
+ throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
11840
+ }
11834
11841
  } catch (error) {
11835
11842
  throw this.handleError(error);
11836
11843
  }
@@ -11857,12 +11864,16 @@ var AnthropicProvider = class {
11857
11864
  let currentToolInputJson = "";
11858
11865
  const streamTimeout = this.config.timeout ?? 12e4;
11859
11866
  let lastActivityTime = Date.now();
11860
- const checkTimeout = () => {
11867
+ const timeoutController = new AbortController();
11868
+ const timeoutInterval = setInterval(() => {
11861
11869
  if (Date.now() - lastActivityTime > streamTimeout) {
11862
- throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
11870
+ clearInterval(timeoutInterval);
11871
+ timeoutController.abort();
11863
11872
  }
11864
- };
11865
- const timeoutInterval = setInterval(checkTimeout, 5e3);
11873
+ }, 5e3);
11874
+ timeoutController.signal.addEventListener("abort", () => stream.controller.abort(), {
11875
+ once: true
11876
+ });
11866
11877
  try {
11867
11878
  let streamStopReason;
11868
11879
  for await (const event of stream) {
@@ -11947,6 +11958,9 @@ var AnthropicProvider = class {
11947
11958
  } finally {
11948
11959
  clearInterval(timeoutInterval);
11949
11960
  }
11961
+ if (timeoutController.signal.aborted) {
11962
+ throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
11963
+ }
11950
11964
  } catch (error) {
11951
11965
  throw this.handleError(error);
11952
11966
  }
@@ -12178,7 +12192,7 @@ function createKimiCodeProvider(config) {
12178
12192
  }
12179
12193
  return provider;
12180
12194
  }
12181
- var DEFAULT_MODEL2 = "gpt-5.3-codex";
12195
+ var DEFAULT_MODEL2 = "gpt-5.4-codex";
12182
12196
  var CONTEXT_WINDOWS2 = {
12183
12197
  // OpenAI models
12184
12198
  "gpt-4o": 128e3,
@@ -12201,6 +12215,7 @@ var CONTEXT_WINDOWS2 = {
12201
12215
  "gpt-5.2-instant": 4e5,
12202
12216
  "gpt-5.2-pro": 4e5,
12203
12217
  "gpt-5.3-codex": 4e5,
12218
+ "gpt-5.4-codex": 4e5,
12204
12219
  // Kimi/Moonshot models
12205
12220
  "kimi-k2.5": 262144,
12206
12221
  "kimi-k2-0324": 131072,
@@ -12257,7 +12272,7 @@ var CONTEXT_WINDOWS2 = {
12257
12272
  "microsoft/Phi-4": 16384,
12258
12273
  // OpenRouter model IDs
12259
12274
  "anthropic/claude-opus-4-6": 2e5,
12260
- "openai/gpt-5.3-codex": 4e5,
12275
+ "openai/gpt-5.4-codex": 4e5,
12261
12276
  "google/gemini-3-flash-preview": 1e6,
12262
12277
  "meta-llama/llama-3.3-70b-instruct": 128e3
12263
12278
  };
@@ -12282,6 +12297,9 @@ var LOCAL_MODEL_PATTERNS = [
12282
12297
  "starcoder"
12283
12298
  ];
12284
12299
  var MODELS_WITH_THINKING_MODE = ["kimi-k2.5", "kimi-k2-0324", "kimi-latest"];
12300
+ function needsResponsesApi(model) {
12301
+ return model.includes("codex") || model.startsWith("gpt-5") || model.startsWith("o4-") || model.startsWith("o3-");
12302
+ }
12285
12303
  var OpenAIProvider = class {
12286
12304
  id;
12287
12305
  name;
@@ -12351,9 +12369,12 @@ var OpenAIProvider = class {
12351
12369
  */
12352
12370
  async chat(messages, options) {
12353
12371
  this.ensureInitialized();
12372
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
12373
+ if (needsResponsesApi(model)) {
12374
+ return this.chatViaResponses(messages, options);
12375
+ }
12354
12376
  return withRetry(async () => {
12355
12377
  try {
12356
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
12357
12378
  const supportsTemp = this.supportsTemperature(model);
12358
12379
  const response = await this.client.chat.completions.create({
12359
12380
  model,
@@ -12385,9 +12406,12 @@ var OpenAIProvider = class {
12385
12406
  */
12386
12407
  async chatWithTools(messages, options) {
12387
12408
  this.ensureInitialized();
12409
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
12410
+ if (needsResponsesApi(model)) {
12411
+ return this.chatWithToolsViaResponses(messages, options);
12412
+ }
12388
12413
  return withRetry(async () => {
12389
12414
  try {
12390
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
12391
12415
  const supportsTemp = this.supportsTemperature(model);
12392
12416
  const extraBody = this.getExtraBody(model);
12393
12417
  const requestParams = {
@@ -12429,8 +12453,12 @@ var OpenAIProvider = class {
12429
12453
  */
12430
12454
  async *stream(messages, options) {
12431
12455
  this.ensureInitialized();
12456
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
12457
+ if (needsResponsesApi(model)) {
12458
+ yield* this.streamViaResponses(messages, options);
12459
+ return;
12460
+ }
12432
12461
  try {
12433
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
12434
12462
  const supportsTemp = this.supportsTemperature(model);
12435
12463
  const stream = await this.client.chat.completions.create({
12436
12464
  model,
@@ -12460,8 +12488,12 @@ var OpenAIProvider = class {
12460
12488
  */
12461
12489
  async *streamWithTools(messages, options) {
12462
12490
  this.ensureInitialized();
12491
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
12492
+ if (needsResponsesApi(model)) {
12493
+ yield* this.streamWithToolsViaResponses(messages, options);
12494
+ return;
12495
+ }
12463
12496
  try {
12464
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
12465
12497
  const supportsTemp = this.supportsTemperature(model);
12466
12498
  const extraBody = this.getExtraBody(model);
12467
12499
  const requestParams = {
@@ -12484,12 +12516,16 @@ var OpenAIProvider = class {
12484
12516
  const toolCallBuilders = /* @__PURE__ */ new Map();
12485
12517
  const streamTimeout = this.config.timeout ?? 12e4;
12486
12518
  let lastActivityTime = Date.now();
12487
- const checkTimeout = () => {
12519
+ const timeoutController = new AbortController();
12520
+ const timeoutInterval = setInterval(() => {
12488
12521
  if (Date.now() - lastActivityTime > streamTimeout) {
12489
- throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
12522
+ clearInterval(timeoutInterval);
12523
+ timeoutController.abort();
12490
12524
  }
12491
- };
12492
- const timeoutInterval = setInterval(checkTimeout, 5e3);
12525
+ }, 5e3);
12526
+ timeoutController.signal.addEventListener("abort", () => stream.controller.abort(), {
12527
+ once: true
12528
+ });
12493
12529
  const providerName = this.name;
12494
12530
  const parseArguments = (builder) => {
12495
12531
  let input = {};
@@ -12593,6 +12629,9 @@ var OpenAIProvider = class {
12593
12629
  } finally {
12594
12630
  clearInterval(timeoutInterval);
12595
12631
  }
12632
+ if (timeoutController.signal.aborted) {
12633
+ throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
12634
+ }
12596
12635
  } catch (error) {
12597
12636
  throw this.handleError(error);
12598
12637
  }
@@ -12925,1010 +12964,1045 @@ var OpenAIProvider = class {
12925
12964
  cause: error instanceof Error ? error : void 0
12926
12965
  });
12927
12966
  }
12928
- };
12929
- function createKimiProvider(config) {
12930
- const provider = new OpenAIProvider("kimi", "Kimi (Moonshot)");
12931
- const kimiConfig = {
12932
- ...config,
12933
- baseUrl: config?.baseUrl ?? process.env["KIMI_BASE_URL"] ?? "https://api.moonshot.ai/v1",
12934
- apiKey: config?.apiKey ?? process.env["KIMI_API_KEY"] ?? process.env["MOONSHOT_API_KEY"],
12935
- model: config?.model ?? "kimi-k2.5"
12936
- };
12937
- if (kimiConfig.apiKey) {
12938
- provider.initialize(kimiConfig).catch(() => {
12939
- });
12940
- }
12941
- return provider;
12942
- }
12943
- var OAUTH_CONFIGS = {
12967
+ // --- Responses API support (GPT-5+, Codex, o3, o4 models) ---
12944
12968
  /**
12945
- * OpenAI OAuth (ChatGPT Plus/Pro subscriptions)
12946
- * Uses the official Codex client ID (same as OpenCode, Codex CLI, etc.)
12969
+ * Simple chat via Responses API (no tools)
12947
12970
  */
12948
- openai: {
12949
- provider: "openai",
12950
- clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
12951
- authorizationEndpoint: "https://auth.openai.com/oauth/authorize",
12952
- tokenEndpoint: "https://auth.openai.com/oauth/token",
12953
- deviceAuthEndpoint: "https://auth.openai.com/oauth/device/code",
12954
- verificationUri: "https://chatgpt.com/codex/device",
12955
- scopes: ["openid", "profile", "email", "offline_access"],
12956
- extraAuthParams: {
12957
- id_token_add_organizations: "true",
12958
- codex_cli_simplified_flow: "true",
12959
- originator: "opencode"
12960
- }
12961
- }
12962
- // NOTE: Gemini OAuth removed - Google's client ID is restricted to official apps
12963
- // Use API Key (https://aistudio.google.com/apikey) or gcloud ADC instead
12964
- };
12965
- async function refreshAccessToken(provider, refreshToken) {
12966
- const config = OAUTH_CONFIGS[provider];
12967
- if (!config) {
12968
- throw new Error(`OAuth not supported for provider: ${provider}`);
12969
- }
12970
- const body = new URLSearchParams({
12971
- grant_type: "refresh_token",
12972
- client_id: config.clientId,
12973
- refresh_token: refreshToken
12974
- });
12975
- const response = await fetch(config.tokenEndpoint, {
12976
- method: "POST",
12977
- headers: {
12978
- "Content-Type": "application/x-www-form-urlencoded"
12979
- },
12980
- body: body.toString()
12981
- });
12982
- if (!response.ok) {
12983
- const error = await response.text();
12984
- throw new Error(`Token refresh failed: ${error}`);
12985
- }
12986
- const data = await response.json();
12987
- return {
12988
- accessToken: data.access_token,
12989
- refreshToken: data.refresh_token || refreshToken,
12990
- expiresAt: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0,
12991
- tokenType: data.token_type
12992
- };
12993
- }
12994
- function getTokenStoragePath(provider) {
12995
- const home = process.env.HOME || process.env.USERPROFILE || "";
12996
- return path17.join(home, ".coco", "tokens", `${provider}.json`);
12997
- }
12998
- async function saveTokens(provider, tokens) {
12999
- const filePath = getTokenStoragePath(provider);
13000
- const dir = path17.dirname(filePath);
13001
- await fs16.mkdir(dir, { recursive: true, mode: 448 });
13002
- await fs16.writeFile(filePath, JSON.stringify(tokens, null, 2), { mode: 384 });
13003
- }
13004
- async function loadTokens(provider) {
13005
- const filePath = getTokenStoragePath(provider);
13006
- try {
13007
- const content = await fs16.readFile(filePath, "utf-8");
13008
- return JSON.parse(content);
13009
- } catch {
13010
- return null;
12971
+ async chatViaResponses(messages, options) {
12972
+ this.ensureInitialized();
12973
+ return withRetry(async () => {
12974
+ try {
12975
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
12976
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
12977
+ const response = await this.client.responses.create({
12978
+ model,
12979
+ input,
12980
+ instructions: instructions ?? void 0,
12981
+ max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
12982
+ temperature: options?.temperature ?? this.config.temperature ?? 0,
12983
+ store: false
12984
+ });
12985
+ return {
12986
+ id: response.id,
12987
+ content: response.output_text ?? "",
12988
+ stopReason: response.status === "completed" ? "end_turn" : "max_tokens",
12989
+ usage: {
12990
+ inputTokens: response.usage?.input_tokens ?? 0,
12991
+ outputTokens: response.usage?.output_tokens ?? 0
12992
+ },
12993
+ model: String(response.model)
12994
+ };
12995
+ } catch (error) {
12996
+ throw this.handleError(error);
12997
+ }
12998
+ }, this.retryConfig);
13011
12999
  }
13012
- }
13013
- async function deleteTokens(provider) {
13014
- const filePath = getTokenStoragePath(provider);
13015
- try {
13016
- await fs16.unlink(filePath);
13017
- } catch {
13000
+ /**
13001
+ * Chat with tools via Responses API
13002
+ */
13003
+ async chatWithToolsViaResponses(messages, options) {
13004
+ this.ensureInitialized();
13005
+ return withRetry(async () => {
13006
+ try {
13007
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
13008
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
13009
+ const tools = this.convertToolsForResponses(options.tools);
13010
+ const response = await this.client.responses.create({
13011
+ model,
13012
+ input,
13013
+ instructions: instructions ?? void 0,
13014
+ tools,
13015
+ max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
13016
+ temperature: options?.temperature ?? this.config.temperature ?? 0,
13017
+ store: false
13018
+ });
13019
+ let content = "";
13020
+ const toolCalls = [];
13021
+ for (const item of response.output) {
13022
+ if (item.type === "message") {
13023
+ for (const part of item.content) {
13024
+ if (part.type === "output_text") {
13025
+ content += part.text;
13026
+ }
13027
+ }
13028
+ } else if (item.type === "function_call") {
13029
+ toolCalls.push({
13030
+ id: item.call_id,
13031
+ name: item.name,
13032
+ input: this.parseResponsesArguments(item.arguments)
13033
+ });
13034
+ }
13035
+ }
13036
+ return {
13037
+ id: response.id,
13038
+ content,
13039
+ stopReason: toolCalls.length > 0 ? "tool_use" : "end_turn",
13040
+ usage: {
13041
+ inputTokens: response.usage?.input_tokens ?? 0,
13042
+ outputTokens: response.usage?.output_tokens ?? 0
13043
+ },
13044
+ model: String(response.model),
13045
+ toolCalls
13046
+ };
13047
+ } catch (error) {
13048
+ throw this.handleError(error);
13049
+ }
13050
+ }, this.retryConfig);
13018
13051
  }
13019
- }
13020
- function isTokenExpired(tokens) {
13021
- if (!tokens.expiresAt) return false;
13022
- return Date.now() >= tokens.expiresAt - 5 * 60 * 1e3;
13023
- }
13024
- async function getValidAccessToken(provider) {
13025
- const config = OAUTH_CONFIGS[provider];
13026
- if (!config) return null;
13027
- const tokens = await loadTokens(provider);
13028
- if (!tokens) return null;
13029
- if (isTokenExpired(tokens)) {
13030
- if (tokens.refreshToken) {
13052
+ /**
13053
+ * Stream via Responses API (no tools)
13054
+ */
13055
+ async *streamViaResponses(messages, options) {
13056
+ this.ensureInitialized();
13057
+ try {
13058
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
13059
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
13060
+ const stream = await this.client.responses.create({
13061
+ model,
13062
+ input,
13063
+ instructions: instructions ?? void 0,
13064
+ max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
13065
+ temperature: options?.temperature ?? this.config.temperature ?? 0,
13066
+ store: false,
13067
+ stream: true
13068
+ });
13069
+ const streamTimeout = this.config.timeout ?? 12e4;
13070
+ let lastActivityTime = Date.now();
13071
+ const timeoutController = new AbortController();
13072
+ const timeoutInterval = setInterval(() => {
13073
+ if (Date.now() - lastActivityTime > streamTimeout) {
13074
+ clearInterval(timeoutInterval);
13075
+ timeoutController.abort();
13076
+ }
13077
+ }, 5e3);
13078
+ timeoutController.signal.addEventListener(
13079
+ "abort",
13080
+ () => stream.controller?.abort(),
13081
+ { once: true }
13082
+ );
13031
13083
  try {
13032
- const newTokens = await refreshAccessToken(provider, tokens.refreshToken);
13033
- await saveTokens(provider, newTokens);
13034
- return { accessToken: newTokens.accessToken, isNew: true };
13035
- } catch {
13036
- await deleteTokens(provider);
13037
- return null;
13084
+ for await (const event of stream) {
13085
+ lastActivityTime = Date.now();
13086
+ if (event.type === "response.output_text.delta") {
13087
+ yield { type: "text", text: event.delta };
13088
+ } else if (event.type === "response.completed") {
13089
+ yield { type: "done", stopReason: "end_turn" };
13090
+ }
13091
+ }
13092
+ } finally {
13093
+ clearInterval(timeoutInterval);
13038
13094
  }
13095
+ if (timeoutController.signal.aborted) {
13096
+ throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
13097
+ }
13098
+ } catch (error) {
13099
+ throw this.handleError(error);
13100
+ }
13101
+ }
13102
+ /**
13103
+ * Stream with tools via Responses API
13104
+ *
13105
+ * IMPORTANT: fnCallBuilders is keyed by output item ID (fc.id), NOT by
13106
+ * call_id. The streaming events (function_call_arguments.delta/done) use
13107
+ * item_id which references the output item's id field, not call_id.
13108
+ */
13109
+ async *streamWithToolsViaResponses(messages, options) {
13110
+ this.ensureInitialized();
13111
+ try {
13112
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL2;
13113
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
13114
+ const tools = options.tools.length > 0 ? this.convertToolsForResponses(options.tools) : void 0;
13115
+ const requestParams = {
13116
+ model,
13117
+ input,
13118
+ instructions: instructions ?? void 0,
13119
+ max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
13120
+ temperature: options?.temperature ?? this.config.temperature ?? 0,
13121
+ store: false,
13122
+ stream: true
13123
+ };
13124
+ if (tools) {
13125
+ requestParams.tools = tools;
13126
+ }
13127
+ const stream = await this.client.responses.create(
13128
+ requestParams
13129
+ );
13130
+ const fnCallBuilders = /* @__PURE__ */ new Map();
13131
+ const streamTimeout = this.config.timeout ?? 12e4;
13132
+ let lastActivityTime = Date.now();
13133
+ const timeoutController = new AbortController();
13134
+ const timeoutInterval = setInterval(() => {
13135
+ if (Date.now() - lastActivityTime > streamTimeout) {
13136
+ clearInterval(timeoutInterval);
13137
+ timeoutController.abort();
13138
+ }
13139
+ }, 5e3);
13140
+ timeoutController.signal.addEventListener(
13141
+ "abort",
13142
+ () => stream.controller?.abort(),
13143
+ { once: true }
13144
+ );
13145
+ try {
13146
+ for await (const event of stream) {
13147
+ lastActivityTime = Date.now();
13148
+ switch (event.type) {
13149
+ case "response.output_text.delta":
13150
+ yield { type: "text", text: event.delta };
13151
+ break;
13152
+ case "response.output_item.added":
13153
+ if (event.item.type === "function_call") {
13154
+ const fc = event.item;
13155
+ const itemKey = fc.id ?? fc.call_id;
13156
+ fnCallBuilders.set(itemKey, {
13157
+ callId: fc.call_id,
13158
+ name: fc.name,
13159
+ arguments: ""
13160
+ });
13161
+ yield {
13162
+ type: "tool_use_start",
13163
+ toolCall: { id: fc.call_id, name: fc.name }
13164
+ };
13165
+ }
13166
+ break;
13167
+ case "response.function_call_arguments.delta":
13168
+ {
13169
+ const builder = fnCallBuilders.get(event.item_id);
13170
+ if (builder) {
13171
+ builder.arguments += event.delta;
13172
+ }
13173
+ }
13174
+ break;
13175
+ case "response.function_call_arguments.done":
13176
+ {
13177
+ const builder = fnCallBuilders.get(event.item_id);
13178
+ if (builder) {
13179
+ yield {
13180
+ type: "tool_use_end",
13181
+ toolCall: {
13182
+ id: builder.callId,
13183
+ name: builder.name,
13184
+ input: this.parseResponsesArguments(event.arguments)
13185
+ }
13186
+ };
13187
+ fnCallBuilders.delete(event.item_id);
13188
+ }
13189
+ }
13190
+ break;
13191
+ case "response.completed":
13192
+ {
13193
+ for (const [, builder] of fnCallBuilders) {
13194
+ yield {
13195
+ type: "tool_use_end",
13196
+ toolCall: {
13197
+ id: builder.callId,
13198
+ name: builder.name,
13199
+ input: this.parseResponsesArguments(builder.arguments)
13200
+ }
13201
+ };
13202
+ }
13203
+ fnCallBuilders.clear();
13204
+ const hasToolCalls = event.response.output.some(
13205
+ (i) => i.type === "function_call"
13206
+ );
13207
+ yield {
13208
+ type: "done",
13209
+ stopReason: hasToolCalls ? "tool_use" : "end_turn"
13210
+ };
13211
+ }
13212
+ break;
13213
+ }
13214
+ }
13215
+ } finally {
13216
+ clearInterval(timeoutInterval);
13217
+ }
13218
+ if (timeoutController.signal.aborted) {
13219
+ throw new Error(`Stream timeout: No response from LLM for ${streamTimeout / 1e3}s`);
13220
+ }
13221
+ } catch (error) {
13222
+ throw this.handleError(error);
13223
+ }
13224
+ }
13225
+ // --- Responses API conversion helpers ---
13226
+ /**
13227
+ * Convert internal messages to Responses API input format.
13228
+ *
13229
+ * The Responses API uses a flat array of input items instead of the
13230
+ * chat completions messages array.
13231
+ */
13232
+ convertToResponsesInput(messages, systemPrompt) {
13233
+ const input = [];
13234
+ let instructions = null;
13235
+ if (systemPrompt) {
13236
+ instructions = systemPrompt;
13237
+ }
13238
+ for (const msg of messages) {
13239
+ if (msg.role === "system") {
13240
+ instructions = (instructions ? instructions + "\n\n" : "") + this.contentToString(msg.content);
13241
+ } else if (msg.role === "user") {
13242
+ if (Array.isArray(msg.content) && msg.content.some((b) => b.type === "tool_result")) {
13243
+ for (const block of msg.content) {
13244
+ if (block.type === "tool_result") {
13245
+ const tr = block;
13246
+ input.push({
13247
+ type: "function_call_output",
13248
+ call_id: tr.tool_use_id,
13249
+ output: tr.content
13250
+ });
13251
+ }
13252
+ }
13253
+ } else if (Array.isArray(msg.content) && msg.content.some((b) => b.type === "image")) {
13254
+ const parts = [];
13255
+ for (const block of msg.content) {
13256
+ if (block.type === "text") {
13257
+ parts.push({ type: "input_text", text: block.text });
13258
+ } else if (block.type === "image") {
13259
+ const imgBlock = block;
13260
+ parts.push({
13261
+ type: "input_image",
13262
+ image_url: `data:${imgBlock.source.media_type};base64,${imgBlock.source.data}`,
13263
+ detail: "auto"
13264
+ });
13265
+ }
13266
+ }
13267
+ input.push({
13268
+ role: "user",
13269
+ content: parts
13270
+ });
13271
+ } else {
13272
+ input.push({
13273
+ role: "user",
13274
+ content: this.contentToString(msg.content)
13275
+ });
13276
+ }
13277
+ } else if (msg.role === "assistant") {
13278
+ if (typeof msg.content === "string") {
13279
+ input.push({ role: "assistant", content: msg.content });
13280
+ } else if (Array.isArray(msg.content)) {
13281
+ const textParts = [];
13282
+ for (const block of msg.content) {
13283
+ if (block.type === "text") {
13284
+ textParts.push(block.text);
13285
+ } else if (block.type === "tool_use") {
13286
+ if (textParts.length > 0) {
13287
+ input.push({ role: "assistant", content: textParts.join("") });
13288
+ textParts.length = 0;
13289
+ }
13290
+ input.push({
13291
+ type: "function_call",
13292
+ call_id: block.id,
13293
+ name: block.name,
13294
+ arguments: JSON.stringify(block.input)
13295
+ });
13296
+ }
13297
+ }
13298
+ if (textParts.length > 0) {
13299
+ input.push({ role: "assistant", content: textParts.join("") });
13300
+ }
13301
+ }
13302
+ }
13303
+ }
13304
+ return { input, instructions };
13305
+ }
13306
+ /**
13307
+ * Convert tool definitions to Responses API FunctionTool format
13308
+ */
13309
+ convertToolsForResponses(tools) {
13310
+ return tools.map((tool) => ({
13311
+ type: "function",
13312
+ name: tool.name,
13313
+ description: tool.description ?? void 0,
13314
+ parameters: tool.input_schema ?? null,
13315
+ strict: false
13316
+ }));
13317
+ }
13318
+ /**
13319
+ * Parse tool call arguments with jsonrepair fallback (Responses API)
13320
+ */
13321
+ parseResponsesArguments(args) {
13322
+ try {
13323
+ return args ? JSON.parse(args) : {};
13324
+ } catch {
13325
+ try {
13326
+ if (args) {
13327
+ const repaired = jsonrepair(args);
13328
+ return JSON.parse(repaired);
13329
+ }
13330
+ } catch {
13331
+ console.error(`[${this.name}] Cannot parse tool arguments: ${args.slice(0, 200)}`);
13332
+ }
13333
+ return {};
13039
13334
  }
13040
- await deleteTokens(provider);
13041
- return null;
13042
- }
13043
- return { accessToken: tokens.accessToken, isNew: false };
13044
- }
13045
- function detectWSL() {
13046
- if (process.env.WSL_DISTRO_NAME || process.env.WSLENV) return true;
13047
- try {
13048
- return /microsoft/i.test(readFileSync("/proc/version", "utf-8"));
13049
- } catch {
13050
- return false;
13051
13335
  }
13052
- }
13053
- var isWSL = detectWSL();
13054
- var COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
13055
- var COPILOT_BASE_URLS = {
13056
- individual: "https://api.githubcopilot.com",
13057
- business: "https://api.business.githubcopilot.com",
13058
- enterprise: "https://api.enterprise.githubcopilot.com"
13059
13336
  };
13060
- var DEFAULT_COPILOT_BASE_URL = "https://api.githubcopilot.com";
13061
- var REFRESH_BUFFER_MS = 6e4;
13062
- var CopilotAuthError = class extends Error {
13063
- constructor(message, permanent) {
13064
- super(message);
13065
- this.permanent = permanent;
13066
- this.name = "CopilotAuthError";
13337
+ function createKimiProvider(config) {
13338
+ const provider = new OpenAIProvider("kimi", "Kimi (Moonshot)");
13339
+ const kimiConfig = {
13340
+ ...config,
13341
+ baseUrl: config?.baseUrl ?? process.env["KIMI_BASE_URL"] ?? "https://api.moonshot.ai/v1",
13342
+ apiKey: config?.apiKey ?? process.env["KIMI_API_KEY"] ?? process.env["MOONSHOT_API_KEY"],
13343
+ model: config?.model ?? "kimi-k2.5"
13344
+ };
13345
+ if (kimiConfig.apiKey) {
13346
+ provider.initialize(kimiConfig).catch(() => {
13347
+ });
13067
13348
  }
13349
+ return provider;
13350
+ }
13351
+ var OAUTH_CONFIGS = {
13352
+ /**
13353
+ * OpenAI OAuth (ChatGPT Plus/Pro subscriptions)
13354
+ * Uses the official Codex client ID (same as OpenCode, Codex CLI, etc.)
13355
+ */
13356
+ openai: {
13357
+ provider: "openai",
13358
+ clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
13359
+ authorizationEndpoint: "https://auth.openai.com/oauth/authorize",
13360
+ tokenEndpoint: "https://auth.openai.com/oauth/token",
13361
+ deviceAuthEndpoint: "https://auth.openai.com/oauth/device/code",
13362
+ verificationUri: "https://chatgpt.com/codex/device",
13363
+ scopes: ["openid", "profile", "email", "offline_access"],
13364
+ extraAuthParams: {
13365
+ id_token_add_organizations: "true",
13366
+ codex_cli_simplified_flow: "true",
13367
+ originator: "opencode"
13368
+ }
13369
+ }
13370
+ // NOTE: Gemini OAuth removed - Google's client ID is restricted to official apps
13371
+ // Use API Key (https://aistudio.google.com/apikey) or gcloud ADC instead
13068
13372
  };
13069
- async function exchangeForCopilotToken(githubToken) {
13070
- const response = await fetch(COPILOT_TOKEN_URL, {
13071
- method: "GET",
13373
+ async function refreshAccessToken(provider, refreshToken) {
13374
+ const config = OAUTH_CONFIGS[provider];
13375
+ if (!config) {
13376
+ throw new Error(`OAuth not supported for provider: ${provider}`);
13377
+ }
13378
+ const body = new URLSearchParams({
13379
+ grant_type: "refresh_token",
13380
+ client_id: config.clientId,
13381
+ refresh_token: refreshToken
13382
+ });
13383
+ const response = await fetch(config.tokenEndpoint, {
13384
+ method: "POST",
13072
13385
  headers: {
13073
- Authorization: `token ${githubToken}`,
13074
- Accept: "application/json",
13075
- "User-Agent": "Corbat-Coco/1.0"
13076
- }
13386
+ "Content-Type": "application/x-www-form-urlencoded"
13387
+ },
13388
+ body: body.toString()
13077
13389
  });
13078
13390
  if (!response.ok) {
13079
13391
  const error = await response.text();
13080
- if (response.status === 401) {
13081
- throw new CopilotAuthError(
13082
- "GitHub token is invalid or expired. Please re-authenticate with /provider copilot.",
13083
- true
13084
- );
13085
- }
13086
- if (response.status === 403) {
13087
- throw new CopilotAuthError(
13088
- "GitHub Copilot is not enabled for this account.\n Please ensure you have an active Copilot subscription:\n https://github.com/settings/copilot",
13089
- true
13090
- );
13091
- }
13092
- throw new Error(`Copilot token exchange failed: ${response.status} - ${error}`);
13093
- }
13094
- return await response.json();
13095
- }
13096
- function getCopilotBaseUrl(accountType) {
13097
- if (accountType && accountType in COPILOT_BASE_URLS) {
13098
- return COPILOT_BASE_URLS[accountType];
13392
+ throw new Error(`Token refresh failed: ${error}`);
13099
13393
  }
13100
- return DEFAULT_COPILOT_BASE_URL;
13394
+ const data = await response.json();
13395
+ return {
13396
+ accessToken: data.access_token,
13397
+ refreshToken: data.refresh_token || refreshToken,
13398
+ expiresAt: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0,
13399
+ tokenType: data.token_type
13400
+ };
13101
13401
  }
13102
- function getCopilotCredentialsPath() {
13402
+ function getTokenStoragePath(provider) {
13103
13403
  const home = process.env.HOME || process.env.USERPROFILE || "";
13104
- return path17.join(home, ".coco", "tokens", "copilot.json");
13404
+ return path17.join(home, ".coco", "tokens", `${provider}.json`);
13105
13405
  }
13106
- async function saveCopilotCredentials(creds) {
13107
- const filePath = getCopilotCredentialsPath();
13406
+ async function saveTokens(provider, tokens) {
13407
+ const filePath = getTokenStoragePath(provider);
13108
13408
  const dir = path17.dirname(filePath);
13109
13409
  await fs16.mkdir(dir, { recursive: true, mode: 448 });
13110
- await fs16.writeFile(filePath, JSON.stringify(creds, null, 2), { mode: 384 });
13111
- }
13112
- var CopilotCredentialsSchema = z.object({
13113
- githubToken: z.string().min(1),
13114
- copilotToken: z.string().optional(),
13115
- copilotTokenExpiresAt: z.number().optional(),
13116
- accountType: z.string().optional()
13117
- });
13118
- async function loadCopilotCredentials() {
13119
- try {
13120
- const content = await fs16.readFile(getCopilotCredentialsPath(), "utf-8");
13121
- const parsed = CopilotCredentialsSchema.safeParse(JSON.parse(content));
13122
- return parsed.success ? parsed.data : null;
13123
- } catch {
13124
- return null;
13125
- }
13410
+ await fs16.writeFile(filePath, JSON.stringify(tokens, null, 2), { mode: 384 });
13126
13411
  }
13127
- async function deleteCopilotCredentials() {
13412
+ async function loadTokens(provider) {
13413
+ const filePath = getTokenStoragePath(provider);
13128
13414
  try {
13129
- await fs16.unlink(getCopilotCredentialsPath());
13415
+ const content = await fs16.readFile(filePath, "utf-8");
13416
+ return JSON.parse(content);
13130
13417
  } catch {
13131
- }
13132
- }
13133
- function isCopilotTokenExpired(creds) {
13134
- if (!creds.copilotToken || !creds.copilotTokenExpiresAt) return true;
13135
- return Date.now() >= creds.copilotTokenExpiresAt - REFRESH_BUFFER_MS;
13136
- }
13137
- async function getValidCopilotToken() {
13138
- const creds = await loadCopilotCredentials();
13139
- if (!creds) return null;
13140
- const envToken = process.env["GITHUB_TOKEN"] || process.env["GH_TOKEN"];
13141
- const githubToken = envToken || creds.githubToken;
13142
- if (!isCopilotTokenExpired(creds) && creds.copilotToken) {
13143
- return {
13144
- token: creds.copilotToken,
13145
- baseUrl: getCopilotBaseUrl(creds.accountType),
13146
- isNew: false
13147
- };
13148
- }
13149
- try {
13150
- const copilotToken = await exchangeForCopilotToken(githubToken);
13151
- const updatedCreds = {
13152
- ...creds,
13153
- githubToken: creds.githubToken,
13154
- copilotToken: copilotToken.token,
13155
- copilotTokenExpiresAt: copilotToken.expires_at * 1e3,
13156
- accountType: copilotToken.annotations?.copilot_plan ?? creds.accountType
13157
- };
13158
- await saveCopilotCredentials(updatedCreds);
13159
- return {
13160
- token: copilotToken.token,
13161
- baseUrl: getCopilotBaseUrl(updatedCreds.accountType),
13162
- isNew: true
13163
- };
13164
- } catch (error) {
13165
- if (error instanceof CopilotAuthError && error.permanent) {
13166
- await deleteCopilotCredentials();
13167
- return null;
13168
- }
13169
- throw error;
13170
- }
13171
- }
13172
-
13173
- // src/auth/flow.ts
13174
- promisify(execFile);
13175
- var execAsync2 = promisify(exec);
13176
- async function getADCAccessToken() {
13177
- try {
13178
- const { stdout } = await execAsync2("gcloud auth application-default print-access-token", {
13179
- timeout: 1e4
13180
- });
13181
- const accessToken = stdout.trim();
13182
- if (!accessToken) return null;
13183
- const expiresAt = Date.now() + 55 * 60 * 1e3;
13184
- return {
13185
- accessToken,
13186
- expiresAt
13187
- };
13188
- } catch (error) {
13189
- const message = error instanceof Error ? error.message : String(error);
13190
- if (message.includes("not logged in") || message.includes("no application default credentials")) {
13191
- return null;
13192
- }
13193
13418
  return null;
13194
13419
  }
13195
13420
  }
13196
- var cachedToken = null;
13197
- async function getCachedADCToken() {
13198
- if (cachedToken && cachedToken.expiresAt && Date.now() < cachedToken.expiresAt) {
13199
- return cachedToken;
13200
- }
13201
- cachedToken = await getADCAccessToken();
13202
- return cachedToken;
13203
- }
13204
-
13205
- // src/providers/codex.ts
13206
- var CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
13207
- var DEFAULT_MODEL3 = "gpt-5.3-codex";
13208
- var CONTEXT_WINDOWS3 = {
13209
- "gpt-5.3-codex": 2e5,
13210
- "gpt-5.2-codex": 2e5,
13211
- "gpt-5-codex": 2e5,
13212
- "gpt-5.1-codex": 2e5,
13213
- "gpt-5": 2e5,
13214
- "gpt-5.2": 2e5,
13215
- "gpt-5.1": 2e5
13216
- };
13217
- function parseJwtClaims(token) {
13218
- const parts = token.split(".");
13219
- if (parts.length !== 3 || !parts[1]) return void 0;
13421
+ async function deleteTokens(provider) {
13422
+ const filePath = getTokenStoragePath(provider);
13220
13423
  try {
13221
- return JSON.parse(Buffer.from(parts[1], "base64url").toString());
13424
+ await fs16.unlink(filePath);
13222
13425
  } catch {
13223
- return void 0;
13224
13426
  }
13225
13427
  }
13226
- function extractAccountId(accessToken) {
13227
- const claims = parseJwtClaims(accessToken);
13228
- if (!claims) return void 0;
13229
- const auth = claims["https://api.openai.com/auth"];
13230
- return claims["chatgpt_account_id"] || auth?.["chatgpt_account_id"] || claims["organizations"]?.[0]?.id;
13231
- }
13232
- var CodexProvider = class {
13233
- id = "codex";
13234
- name = "OpenAI Codex (ChatGPT Plus/Pro)";
13235
- config = {};
13236
- accessToken = null;
13237
- accountId;
13238
- /**
13239
- * Initialize the provider with OAuth tokens
13240
- */
13241
- async initialize(config) {
13242
- this.config = config;
13243
- const tokenResult = await getValidAccessToken("openai");
13244
- if (tokenResult) {
13245
- this.accessToken = tokenResult.accessToken;
13246
- this.accountId = extractAccountId(tokenResult.accessToken);
13247
- } else if (config.apiKey) {
13248
- this.accessToken = config.apiKey;
13249
- this.accountId = extractAccountId(config.apiKey);
13250
- }
13251
- if (!this.accessToken) {
13252
- throw new ProviderError(
13253
- "No OAuth token found. Please run authentication first with: coco --provider openai",
13254
- { provider: this.id }
13255
- );
13256
- }
13257
- }
13258
- /**
13259
- * Ensure provider is initialized
13260
- */
13261
- ensureInitialized() {
13262
- if (!this.accessToken) {
13263
- throw new ProviderError("Provider not initialized", {
13264
- provider: this.id
13265
- });
13428
+ function isTokenExpired(tokens) {
13429
+ if (!tokens.expiresAt) return false;
13430
+ return Date.now() >= tokens.expiresAt - 5 * 60 * 1e3;
13431
+ }
13432
+ async function getValidAccessToken(provider) {
13433
+ const config = OAUTH_CONFIGS[provider];
13434
+ if (!config) return null;
13435
+ const tokens = await loadTokens(provider);
13436
+ if (!tokens) return null;
13437
+ if (isTokenExpired(tokens)) {
13438
+ if (tokens.refreshToken) {
13439
+ try {
13440
+ const newTokens = await refreshAccessToken(provider, tokens.refreshToken);
13441
+ await saveTokens(provider, newTokens);
13442
+ return { accessToken: newTokens.accessToken, isNew: true };
13443
+ } catch {
13444
+ await deleteTokens(provider);
13445
+ return null;
13446
+ }
13266
13447
  }
13448
+ await deleteTokens(provider);
13449
+ return null;
13267
13450
  }
13268
- /**
13269
- * Get context window size for a model
13270
- */
13271
- getContextWindow(model) {
13272
- const m = model ?? this.config.model ?? DEFAULT_MODEL3;
13273
- return CONTEXT_WINDOWS3[m] ?? 128e3;
13451
+ return { accessToken: tokens.accessToken, isNew: false };
13452
+ }
13453
+ function detectWSL() {
13454
+ if (process.env.WSL_DISTRO_NAME || process.env.WSLENV) return true;
13455
+ try {
13456
+ return /microsoft/i.test(readFileSync("/proc/version", "utf-8"));
13457
+ } catch {
13458
+ return false;
13274
13459
  }
13275
- /**
13276
- * Count tokens in text (approximate)
13277
- * Uses GPT-4 approximation: ~4 chars per token
13278
- */
13279
- countTokens(text) {
13280
- return Math.ceil(text.length / 4);
13460
+ }
13461
+ var isWSL = detectWSL();
13462
+ var COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
13463
+ var COPILOT_BASE_URLS = {
13464
+ individual: "https://api.githubcopilot.com",
13465
+ business: "https://api.business.githubcopilot.com",
13466
+ enterprise: "https://api.enterprise.githubcopilot.com"
13467
+ };
13468
+ var DEFAULT_COPILOT_BASE_URL = "https://api.githubcopilot.com";
13469
+ var REFRESH_BUFFER_MS = 6e4;
13470
+ var CopilotAuthError = class extends Error {
13471
+ constructor(message, permanent) {
13472
+ super(message);
13473
+ this.permanent = permanent;
13474
+ this.name = "CopilotAuthError";
13281
13475
  }
13282
- /**
13283
- * Check if provider is available (has valid OAuth tokens)
13284
- */
13285
- async isAvailable() {
13286
- try {
13287
- const tokenResult = await getValidAccessToken("openai");
13288
- return tokenResult !== null;
13289
- } catch {
13290
- return false;
13476
+ };
13477
+ async function exchangeForCopilotToken(githubToken) {
13478
+ const response = await fetch(COPILOT_TOKEN_URL, {
13479
+ method: "GET",
13480
+ headers: {
13481
+ Authorization: `token ${githubToken}`,
13482
+ Accept: "application/json",
13483
+ "User-Agent": "Corbat-Coco/1.0"
13291
13484
  }
13292
- }
13293
- /**
13294
- * Make a request to the Codex API
13295
- */
13296
- async makeRequest(body) {
13297
- this.ensureInitialized();
13298
- const headers = {
13299
- "Content-Type": "application/json",
13300
- Authorization: `Bearer ${this.accessToken}`
13301
- };
13302
- if (this.accountId) {
13303
- headers["ChatGPT-Account-Id"] = this.accountId;
13485
+ });
13486
+ if (!response.ok) {
13487
+ const error = await response.text();
13488
+ if (response.status === 401) {
13489
+ throw new CopilotAuthError(
13490
+ "GitHub token is invalid or expired. Please re-authenticate with /provider copilot.",
13491
+ true
13492
+ );
13304
13493
  }
13305
- const response = await fetch(CODEX_API_ENDPOINT, {
13306
- method: "POST",
13307
- headers,
13308
- body: JSON.stringify(body)
13309
- });
13310
- if (!response.ok) {
13311
- const errorText = await response.text();
13312
- throw new ProviderError(`Codex API error: ${response.status} - ${errorText}`, {
13313
- provider: this.id,
13314
- statusCode: response.status
13315
- });
13494
+ if (response.status === 403) {
13495
+ throw new CopilotAuthError(
13496
+ "GitHub Copilot is not enabled for this account.\n Please ensure you have an active Copilot subscription:\n https://github.com/settings/copilot",
13497
+ true
13498
+ );
13316
13499
  }
13317
- return response;
13500
+ throw new Error(`Copilot token exchange failed: ${response.status} - ${error}`);
13318
13501
  }
13319
- /**
13320
- * Extract text content from a message
13321
- */
13322
- extractTextContent(msg) {
13323
- if (typeof msg.content === "string") {
13324
- return msg.content;
13325
- }
13326
- if (Array.isArray(msg.content)) {
13327
- return msg.content.map((part) => {
13328
- if (part.type === "text") return part.text;
13329
- if (part.type === "tool_result") return `Tool result: ${JSON.stringify(part.content)}`;
13330
- return "";
13331
- }).join("\n");
13332
- }
13333
- return "";
13502
+ return await response.json();
13503
+ }
13504
+ function getCopilotBaseUrl(accountType) {
13505
+ if (accountType && accountType in COPILOT_BASE_URLS) {
13506
+ return COPILOT_BASE_URLS[accountType];
13334
13507
  }
13335
- /**
13336
- * Convert messages to Codex Responses API format
13337
- * Codex uses a different format than Chat Completions:
13338
- * {
13339
- * "input": [
13340
- * { "type": "message", "role": "developer|user", "content": [{ "type": "input_text", "text": "..." }] },
13341
- * { "type": "message", "role": "assistant", "content": [{ "type": "output_text", "text": "..." }] }
13342
- * ]
13343
- * }
13344
- *
13345
- * IMPORTANT: User/developer messages use "input_text", assistant messages use "output_text"
13346
- */
13347
- convertMessagesToResponsesFormat(messages) {
13348
- return messages.map((msg) => {
13349
- const text = this.extractTextContent(msg);
13350
- const role = msg.role === "system" ? "developer" : msg.role;
13351
- const contentType = msg.role === "assistant" ? "output_text" : "input_text";
13352
- return {
13353
- type: "message",
13354
- role,
13355
- content: [{ type: contentType, text }]
13356
- };
13357
- });
13508
+ return DEFAULT_COPILOT_BASE_URL;
13509
+ }
13510
+ function getCopilotCredentialsPath() {
13511
+ const home = process.env.HOME || process.env.USERPROFILE || "";
13512
+ return path17.join(home, ".coco", "tokens", "copilot.json");
13513
+ }
13514
+ async function saveCopilotCredentials(creds) {
13515
+ const filePath = getCopilotCredentialsPath();
13516
+ const dir = path17.dirname(filePath);
13517
+ await fs16.mkdir(dir, { recursive: true, mode: 448 });
13518
+ await fs16.writeFile(filePath, JSON.stringify(creds, null, 2), { mode: 384 });
13519
+ }
13520
+ var CopilotCredentialsSchema = z.object({
13521
+ githubToken: z.string().min(1),
13522
+ copilotToken: z.string().optional(),
13523
+ copilotTokenExpiresAt: z.number().optional(),
13524
+ accountType: z.string().optional()
13525
+ });
13526
+ async function loadCopilotCredentials() {
13527
+ try {
13528
+ const content = await fs16.readFile(getCopilotCredentialsPath(), "utf-8");
13529
+ const parsed = CopilotCredentialsSchema.safeParse(JSON.parse(content));
13530
+ return parsed.success ? parsed.data : null;
13531
+ } catch {
13532
+ return null;
13358
13533
  }
13359
- /**
13360
- * Send a chat message using Codex Responses API format
13361
- */
13362
- async chat(messages, options) {
13363
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
13364
- const systemMsg = messages.find((m) => m.role === "system");
13365
- const instructions = systemMsg ? this.extractTextContent(systemMsg) : "You are a helpful coding assistant.";
13366
- const inputMessages = messages.filter((m) => m.role !== "system").map((msg) => this.convertMessagesToResponsesFormat([msg])[0]);
13367
- const body = {
13368
- model,
13369
- instructions,
13370
- input: inputMessages,
13371
- tools: [],
13372
- store: false,
13373
- stream: true
13374
- // Codex API requires streaming
13375
- };
13376
- const response = await this.makeRequest(body);
13377
- if (!response.body) {
13378
- throw new ProviderError("No response body from Codex API", {
13379
- provider: this.id
13380
- });
13381
- }
13382
- const reader = response.body.getReader();
13383
- const decoder = new TextDecoder();
13384
- let buffer = "";
13385
- let content = "";
13386
- let responseId = `codex-${Date.now()}`;
13387
- let inputTokens = 0;
13388
- let outputTokens = 0;
13389
- let status = "completed";
13390
- try {
13391
- while (true) {
13392
- const { done, value } = await reader.read();
13393
- if (done) break;
13394
- buffer += decoder.decode(value, { stream: true });
13395
- const lines = buffer.split("\n");
13396
- buffer = lines.pop() ?? "";
13397
- for (const line of lines) {
13398
- if (line.startsWith("data: ")) {
13399
- const data = line.slice(6).trim();
13400
- if (!data || data === "[DONE]") continue;
13401
- try {
13402
- const parsed = JSON.parse(data);
13403
- if (parsed.id) {
13404
- responseId = parsed.id;
13405
- }
13406
- if (parsed.type === "response.output_text.delta" && parsed.delta) {
13407
- content += parsed.delta;
13408
- } else if (parsed.type === "response.completed" && parsed.response) {
13409
- if (parsed.response.usage) {
13410
- inputTokens = parsed.response.usage.input_tokens ?? 0;
13411
- outputTokens = parsed.response.usage.output_tokens ?? 0;
13412
- }
13413
- status = parsed.response.status ?? "completed";
13414
- } else if (parsed.type === "response.output_text.done" && parsed.text) {
13415
- content = parsed.text;
13416
- }
13417
- } catch {
13418
- }
13419
- }
13420
- }
13421
- }
13422
- } finally {
13423
- reader.releaseLock();
13424
- }
13425
- if (!content) {
13426
- throw new ProviderError("No response content from Codex API", {
13427
- provider: this.id
13428
- });
13429
- }
13430
- const stopReason = status === "completed" ? "end_turn" : status === "incomplete" ? "max_tokens" : "end_turn";
13534
+ }
13535
+ async function deleteCopilotCredentials() {
13536
+ try {
13537
+ await fs16.unlink(getCopilotCredentialsPath());
13538
+ } catch {
13539
+ }
13540
+ }
13541
+ function isCopilotTokenExpired(creds) {
13542
+ if (!creds.copilotToken || !creds.copilotTokenExpiresAt) return true;
13543
+ return Date.now() >= creds.copilotTokenExpiresAt - REFRESH_BUFFER_MS;
13544
+ }
13545
+ async function getValidCopilotToken() {
13546
+ const creds = await loadCopilotCredentials();
13547
+ if (!creds) return null;
13548
+ const envToken = process.env["GITHUB_TOKEN"] || process.env["GH_TOKEN"];
13549
+ const githubToken = envToken || creds.githubToken;
13550
+ if (!isCopilotTokenExpired(creds) && creds.copilotToken) {
13431
13551
  return {
13432
- id: responseId,
13433
- content,
13434
- stopReason,
13435
- model,
13436
- usage: {
13437
- inputTokens,
13438
- outputTokens
13439
- }
13552
+ token: creds.copilotToken,
13553
+ baseUrl: getCopilotBaseUrl(creds.accountType),
13554
+ isNew: false
13440
13555
  };
13441
13556
  }
13442
- /**
13443
- * Send a chat message with tool use
13444
- * Note: Codex Responses API tool support is complex; for now we delegate to chat()
13445
- * and return empty toolCalls. Full tool support can be added later.
13446
- */
13447
- async chatWithTools(messages, options) {
13448
- const response = await this.chat(messages, options);
13557
+ try {
13558
+ const copilotToken = await exchangeForCopilotToken(githubToken);
13559
+ const updatedCreds = {
13560
+ ...creds,
13561
+ githubToken: creds.githubToken,
13562
+ copilotToken: copilotToken.token,
13563
+ copilotTokenExpiresAt: copilotToken.expires_at * 1e3,
13564
+ accountType: copilotToken.annotations?.copilot_plan ?? creds.accountType
13565
+ };
13566
+ await saveCopilotCredentials(updatedCreds);
13449
13567
  return {
13450
- ...response,
13451
- toolCalls: []
13452
- // Tools not yet supported in Codex provider
13568
+ token: copilotToken.token,
13569
+ baseUrl: getCopilotBaseUrl(updatedCreds.accountType),
13570
+ isNew: true
13453
13571
  };
13572
+ } catch (error) {
13573
+ if (error instanceof CopilotAuthError && error.permanent) {
13574
+ await deleteCopilotCredentials();
13575
+ return null;
13576
+ }
13577
+ throw error;
13454
13578
  }
13455
- /**
13456
- * Stream a chat response
13457
- * Note: True streaming with Codex Responses API is complex.
13458
- * For now, we make a non-streaming call and simulate streaming by emitting chunks.
13459
- */
13460
- async *stream(messages, options) {
13461
- const response = await this.chat(messages, options);
13462
- if (response.content) {
13463
- const content = response.content;
13464
- const chunkSize = 20;
13465
- for (let i = 0; i < content.length; i += chunkSize) {
13466
- const chunk = content.slice(i, i + chunkSize);
13467
- yield { type: "text", text: chunk };
13468
- if (i + chunkSize < content.length) {
13469
- await new Promise((resolve3) => setTimeout(resolve3, 5));
13470
- }
13471
- }
13579
+ }
13580
+
13581
+ // src/auth/flow.ts
13582
+ promisify(execFile);
13583
+ var execAsync2 = promisify(exec);
13584
+ async function getADCAccessToken() {
13585
+ try {
13586
+ const { stdout } = await execAsync2("gcloud auth application-default print-access-token", {
13587
+ timeout: 1e4
13588
+ });
13589
+ const accessToken = stdout.trim();
13590
+ if (!accessToken) return null;
13591
+ const expiresAt = Date.now() + 55 * 60 * 1e3;
13592
+ return {
13593
+ accessToken,
13594
+ expiresAt
13595
+ };
13596
+ } catch (error) {
13597
+ const message = error instanceof Error ? error.message : String(error);
13598
+ if (message.includes("not logged in") || message.includes("no application default credentials")) {
13599
+ return null;
13472
13600
  }
13473
- yield { type: "done", stopReason: response.stopReason };
13601
+ return null;
13474
13602
  }
13475
- /**
13476
- * Stream a chat response with tool use
13477
- * Note: Tools and true streaming with Codex Responses API are not yet implemented.
13478
- * For now, we delegate to stream() which uses non-streaming under the hood.
13479
- */
13480
- async *streamWithTools(messages, options) {
13481
- yield* this.stream(messages, options);
13603
+ }
13604
+ var cachedToken = null;
13605
+ async function getCachedADCToken() {
13606
+ if (cachedToken && cachedToken.expiresAt && Date.now() < cachedToken.expiresAt) {
13607
+ return cachedToken;
13482
13608
  }
13483
- };
13484
- var CONTEXT_WINDOWS4 = {
13485
- // Claude models
13486
- "claude-sonnet-4.6": 2e5,
13487
- "claude-opus-4.6": 2e5,
13488
- "claude-sonnet-4.5": 2e5,
13489
- "claude-opus-4.5": 2e5,
13490
- "claude-haiku-4.5": 2e5,
13491
- // OpenAI models — chat/completions
13492
- "gpt-4.1": 1048576,
13493
- // OpenAI models — /responses API (Codex/GPT-5+)
13494
- "gpt-5.3-codex": 4e5,
13495
- "gpt-5.2-codex": 4e5,
13496
- "gpt-5.1-codex-max": 4e5,
13497
- "gpt-5.2": 4e5,
13498
- "gpt-5.1": 4e5,
13499
- // Google models
13500
- "gemini-3.1-pro-preview": 1e6,
13501
- "gemini-3-flash-preview": 1e6,
13502
- "gemini-2.5-pro": 1048576
13503
- };
13504
- var DEFAULT_MODEL4 = "claude-sonnet-4.6";
13505
- var COPILOT_HEADERS = {
13506
- "Copilot-Integration-Id": "vscode-chat",
13507
- "Editor-Version": "vscode/1.99.0",
13508
- "Editor-Plugin-Version": "copilot-chat/0.26.7",
13509
- "X-GitHub-Api-Version": "2025-04-01"
13510
- };
13511
- function needsResponsesApi(model) {
13512
- return model.includes("codex") || model.startsWith("gpt-5") || model.startsWith("o4-") || model.startsWith("o3-");
13609
+ cachedToken = await getADCAccessToken();
13610
+ return cachedToken;
13513
13611
  }
13514
- var CopilotProvider = class extends OpenAIProvider {
13515
- baseUrl = "https://api.githubcopilot.com";
13516
- currentToken = null;
13517
- /** In-flight refresh promise to prevent concurrent token exchanges */
13518
- refreshPromise = null;
13519
- constructor() {
13520
- super("copilot", "GitHub Copilot");
13612
+
13613
+ // src/providers/codex.ts
13614
+ var CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
13615
+ var DEFAULT_MODEL3 = "gpt-5.4-codex";
13616
+ var CONTEXT_WINDOWS3 = {
13617
+ "gpt-5.4-codex": 2e5,
13618
+ "gpt-5.3-codex": 2e5,
13619
+ "gpt-5.2-codex": 2e5,
13620
+ "gpt-5-codex": 2e5,
13621
+ "gpt-5.1-codex": 2e5,
13622
+ "gpt-5": 2e5,
13623
+ "gpt-5.2": 2e5,
13624
+ "gpt-5.1": 2e5
13625
+ };
13626
+ function parseJwtClaims(token) {
13627
+ const parts = token.split(".");
13628
+ if (parts.length !== 3 || !parts[1]) return void 0;
13629
+ try {
13630
+ return JSON.parse(Buffer.from(parts[1], "base64url").toString());
13631
+ } catch {
13632
+ return void 0;
13521
13633
  }
13634
+ }
13635
+ function extractAccountId(accessToken) {
13636
+ const claims = parseJwtClaims(accessToken);
13637
+ if (!claims) return void 0;
13638
+ const auth = claims["https://api.openai.com/auth"];
13639
+ return claims["chatgpt_account_id"] || auth?.["chatgpt_account_id"] || claims["organizations"]?.[0]?.id;
13640
+ }
13641
+ var CodexProvider = class {
13642
+ id = "codex";
13643
+ name = "OpenAI Codex (ChatGPT Plus/Pro)";
13644
+ config = {};
13645
+ accessToken = null;
13646
+ accountId;
13522
13647
  /**
13523
- * Initialize the provider with Copilot credentials.
13524
- *
13525
- * Gets a valid Copilot API token (from cache or by refreshing),
13526
- * then creates an OpenAI client configured for the Copilot endpoint.
13648
+ * Initialize the provider with OAuth tokens
13527
13649
  */
13528
13650
  async initialize(config) {
13529
- this.config = {
13530
- ...config,
13531
- model: config.model ?? DEFAULT_MODEL4
13532
- };
13533
- const tokenResult = await getValidCopilotToken();
13651
+ this.config = config;
13652
+ const tokenResult = await getValidAccessToken("openai");
13534
13653
  if (tokenResult) {
13535
- this.currentToken = tokenResult.token;
13536
- this.baseUrl = tokenResult.baseUrl;
13654
+ this.accessToken = tokenResult.accessToken;
13655
+ this.accountId = extractAccountId(tokenResult.accessToken);
13537
13656
  } else if (config.apiKey) {
13538
- this.currentToken = config.apiKey;
13657
+ this.accessToken = config.apiKey;
13658
+ this.accountId = extractAccountId(config.apiKey);
13539
13659
  }
13540
- if (!this.currentToken) {
13660
+ if (!this.accessToken) {
13541
13661
  throw new ProviderError(
13542
- "No Copilot token found. Please authenticate with: coco --provider copilot",
13662
+ "No OAuth token found. Please run authentication first with: coco --provider openai",
13543
13663
  { provider: this.id }
13544
- );
13545
- }
13546
- this.createCopilotClient();
13547
- }
13548
- /**
13549
- * Create the OpenAI client configured for Copilot API
13550
- */
13551
- createCopilotClient() {
13552
- this.client = new OpenAI({
13553
- apiKey: this.currentToken,
13554
- baseURL: this.config.baseUrl ?? this.baseUrl,
13555
- timeout: this.config.timeout ?? 12e4,
13556
- defaultHeaders: COPILOT_HEADERS
13557
- });
13558
- }
13559
- /**
13560
- * Refresh the Copilot token if expired.
13561
- *
13562
- * Uses a mutex so concurrent callers share a single in-flight token
13563
- * exchange. The slot is cleared inside the IIFE's finally block,
13564
- * which runs after all awaiting callers have resumed.
13565
- */
13566
- async refreshTokenIfNeeded() {
13567
- if (!this.refreshPromise) {
13568
- this.refreshPromise = (async () => {
13569
- try {
13570
- const tokenResult = await getValidCopilotToken();
13571
- if (tokenResult && tokenResult.isNew) {
13572
- this.currentToken = tokenResult.token;
13573
- this.baseUrl = tokenResult.baseUrl;
13574
- this.createCopilotClient();
13575
- }
13576
- } finally {
13577
- this.refreshPromise = null;
13578
- }
13579
- })();
13580
- }
13581
- await this.refreshPromise;
13582
- }
13583
- // --- Override public methods to add token refresh + Responses API routing ---
13584
- async chat(messages, options) {
13585
- await this.refreshTokenIfNeeded();
13586
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL4;
13587
- if (needsResponsesApi(model)) {
13588
- return this.chatViaResponses(messages, options);
13589
- }
13590
- return super.chat(messages, options);
13591
- }
13592
- async chatWithTools(messages, options) {
13593
- await this.refreshTokenIfNeeded();
13594
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL4;
13595
- if (needsResponsesApi(model)) {
13596
- return this.chatWithToolsViaResponses(messages, options);
13597
- }
13598
- return super.chatWithTools(messages, options);
13599
- }
13600
- // Note: Token is refreshed before the stream starts but NOT mid-stream.
13601
- // Copilot tokens last ~25 min. Very long streams may get a 401 mid-stream
13602
- // which surfaces as a ProviderError. The retry layer handles re-attempts.
13603
- async *stream(messages, options) {
13604
- await this.refreshTokenIfNeeded();
13605
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL4;
13606
- if (needsResponsesApi(model)) {
13607
- yield* this.streamViaResponses(messages, options);
13608
- return;
13664
+ );
13609
13665
  }
13610
- yield* super.stream(messages, options);
13611
13666
  }
13612
- async *streamWithTools(messages, options) {
13613
- await this.refreshTokenIfNeeded();
13614
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL4;
13615
- if (needsResponsesApi(model)) {
13616
- yield* this.streamWithToolsViaResponses(messages, options);
13617
- return;
13667
+ /**
13668
+ * Ensure provider is initialized
13669
+ */
13670
+ ensureInitialized() {
13671
+ if (!this.accessToken) {
13672
+ throw new ProviderError("Provider not initialized", {
13673
+ provider: this.id
13674
+ });
13618
13675
  }
13619
- yield* super.streamWithTools(messages, options);
13620
13676
  }
13621
- // --- Responses API implementations ---
13622
13677
  /**
13623
- * Simple chat via Responses API (no tools)
13678
+ * Get context window size for a model
13624
13679
  */
13625
- async chatViaResponses(messages, options) {
13626
- this.ensureInitialized();
13627
- return withRetry(async () => {
13628
- try {
13629
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL4;
13630
- const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
13631
- const response = await this.client.responses.create({
13632
- model,
13633
- input,
13634
- instructions: instructions ?? void 0,
13635
- max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
13636
- temperature: options?.temperature ?? this.config.temperature ?? 0,
13637
- store: false
13638
- });
13639
- return {
13640
- id: response.id,
13641
- content: response.output_text ?? "",
13642
- stopReason: response.status === "completed" ? "end_turn" : "max_tokens",
13643
- usage: {
13644
- inputTokens: response.usage?.input_tokens ?? 0,
13645
- outputTokens: response.usage?.output_tokens ?? 0
13646
- },
13647
- model: String(response.model)
13648
- };
13649
- } catch (error) {
13650
- throw this.handleError(error);
13651
- }
13652
- }, DEFAULT_RETRY_CONFIG);
13680
+ getContextWindow(model) {
13681
+ const m = model ?? this.config.model ?? DEFAULT_MODEL3;
13682
+ return CONTEXT_WINDOWS3[m] ?? 128e3;
13653
13683
  }
13654
13684
  /**
13655
- * Chat with tools via Responses API
13685
+ * Count tokens in text (approximate)
13686
+ * Uses GPT-4 approximation: ~4 chars per token
13656
13687
  */
13657
- async chatWithToolsViaResponses(messages, options) {
13658
- this.ensureInitialized();
13659
- return withRetry(async () => {
13660
- try {
13661
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL4;
13662
- const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
13663
- const tools = this.convertToolsForResponses(options.tools);
13664
- const response = await this.client.responses.create({
13665
- model,
13666
- input,
13667
- instructions: instructions ?? void 0,
13668
- tools,
13669
- max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
13670
- temperature: options?.temperature ?? this.config.temperature ?? 0,
13671
- store: false
13672
- });
13673
- let content = "";
13674
- const toolCalls = [];
13675
- for (const item of response.output) {
13676
- if (item.type === "message") {
13677
- for (const part of item.content) {
13678
- if (part.type === "output_text") {
13679
- content += part.text;
13680
- }
13681
- }
13682
- } else if (item.type === "function_call") {
13683
- toolCalls.push({
13684
- id: item.call_id,
13685
- name: item.name,
13686
- input: this.parseToolArguments(item.arguments)
13687
- });
13688
- }
13689
- }
13690
- return {
13691
- id: response.id,
13692
- content,
13693
- stopReason: response.status === "completed" ? "end_turn" : "tool_use",
13694
- usage: {
13695
- inputTokens: response.usage?.input_tokens ?? 0,
13696
- outputTokens: response.usage?.output_tokens ?? 0
13697
- },
13698
- model: String(response.model),
13699
- toolCalls
13700
- };
13701
- } catch (error) {
13702
- throw this.handleError(error);
13703
- }
13704
- }, DEFAULT_RETRY_CONFIG);
13688
+ countTokens(text) {
13689
+ return Math.ceil(text.length / 4);
13705
13690
  }
13706
13691
  /**
13707
- * Stream via Responses API (no tools)
13692
+ * Check if provider is available (has valid OAuth tokens)
13708
13693
  */
13709
- async *streamViaResponses(messages, options) {
13710
- this.ensureInitialized();
13694
+ async isAvailable() {
13711
13695
  try {
13712
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL4;
13713
- const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
13714
- const stream = await this.client.responses.create({
13715
- model,
13716
- input,
13717
- instructions: instructions ?? void 0,
13718
- max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
13719
- temperature: options?.temperature ?? this.config.temperature ?? 0,
13720
- store: false,
13721
- stream: true
13722
- });
13723
- for await (const event of stream) {
13724
- if (event.type === "response.output_text.delta") {
13725
- yield { type: "text", text: event.delta };
13726
- } else if (event.type === "response.completed") {
13727
- yield { type: "done", stopReason: "end_turn" };
13728
- }
13729
- }
13730
- } catch (error) {
13731
- throw this.handleError(error);
13696
+ const tokenResult = await getValidAccessToken("openai");
13697
+ return tokenResult !== null;
13698
+ } catch {
13699
+ return false;
13732
13700
  }
13733
13701
  }
13734
13702
  /**
13735
- * Stream with tools via Responses API
13703
+ * Make a request to the Codex API
13736
13704
  */
13737
- async *streamWithToolsViaResponses(messages, options) {
13705
+ async makeRequest(body) {
13738
13706
  this.ensureInitialized();
13739
- try {
13740
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL4;
13741
- const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
13742
- const tools = this.convertToolsForResponses(options.tools);
13743
- const stream = await this.client.responses.create({
13744
- model,
13745
- input,
13746
- instructions: instructions ?? void 0,
13747
- tools,
13748
- max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
13749
- temperature: options?.temperature ?? this.config.temperature ?? 0,
13750
- store: false,
13751
- stream: true
13707
+ const headers = {
13708
+ "Content-Type": "application/json",
13709
+ Authorization: `Bearer ${this.accessToken}`
13710
+ };
13711
+ if (this.accountId) {
13712
+ headers["ChatGPT-Account-Id"] = this.accountId;
13713
+ }
13714
+ const response = await fetch(CODEX_API_ENDPOINT, {
13715
+ method: "POST",
13716
+ headers,
13717
+ body: JSON.stringify(body)
13718
+ });
13719
+ if (!response.ok) {
13720
+ const errorText = await response.text();
13721
+ throw new ProviderError(`Codex API error: ${response.status} - ${errorText}`, {
13722
+ provider: this.id,
13723
+ statusCode: response.status
13752
13724
  });
13753
- const fnCallBuilders = /* @__PURE__ */ new Map();
13754
- for await (const event of stream) {
13755
- switch (event.type) {
13756
- case "response.output_text.delta":
13757
- yield { type: "text", text: event.delta };
13758
- break;
13759
- case "response.output_item.added":
13760
- if (event.item.type === "function_call") {
13761
- const fc = event.item;
13762
- fnCallBuilders.set(fc.call_id, {
13763
- callId: fc.call_id,
13764
- name: fc.name,
13765
- arguments: ""
13766
- });
13767
- yield {
13768
- type: "tool_use_start",
13769
- toolCall: { id: fc.call_id, name: fc.name }
13770
- };
13771
- }
13772
- break;
13773
- case "response.function_call_arguments.delta":
13774
- {
13775
- const builder = fnCallBuilders.get(event.item_id);
13776
- if (builder) {
13777
- builder.arguments += event.delta;
13778
- }
13779
- }
13780
- break;
13781
- case "response.function_call_arguments.done":
13782
- {
13783
- const builder = fnCallBuilders.get(event.item_id);
13784
- if (builder) {
13785
- yield {
13786
- type: "tool_use_end",
13787
- toolCall: {
13788
- id: builder.callId,
13789
- name: builder.name,
13790
- input: this.parseToolArguments(event.arguments)
13791
- }
13792
- };
13793
- fnCallBuilders.delete(event.item_id);
13725
+ }
13726
+ return response;
13727
+ }
13728
+ /**
13729
+ * Extract text content from a message
13730
+ */
13731
+ extractTextContent(msg) {
13732
+ if (typeof msg.content === "string") {
13733
+ return msg.content;
13734
+ }
13735
+ if (Array.isArray(msg.content)) {
13736
+ return msg.content.map((part) => {
13737
+ if (part.type === "text") return part.text;
13738
+ if (part.type === "tool_result") return `Tool result: ${JSON.stringify(part.content)}`;
13739
+ return "";
13740
+ }).join("\n");
13741
+ }
13742
+ return "";
13743
+ }
13744
+ /**
13745
+ * Convert messages to Codex Responses API format
13746
+ * Codex uses a different format than Chat Completions:
13747
+ * {
13748
+ * "input": [
13749
+ * { "type": "message", "role": "developer|user", "content": [{ "type": "input_text", "text": "..." }] },
13750
+ * { "type": "message", "role": "assistant", "content": [{ "type": "output_text", "text": "..." }] }
13751
+ * ]
13752
+ * }
13753
+ *
13754
+ * IMPORTANT: User/developer messages use "input_text", assistant messages use "output_text"
13755
+ */
13756
+ convertMessagesToResponsesFormat(messages) {
13757
+ return messages.map((msg) => {
13758
+ const text = this.extractTextContent(msg);
13759
+ const role = msg.role === "system" ? "developer" : msg.role;
13760
+ const contentType = msg.role === "assistant" ? "output_text" : "input_text";
13761
+ return {
13762
+ type: "message",
13763
+ role,
13764
+ content: [{ type: contentType, text }]
13765
+ };
13766
+ });
13767
+ }
13768
+ /**
13769
+ * Send a chat message using Codex Responses API format
13770
+ */
13771
+ async chat(messages, options) {
13772
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
13773
+ const systemMsg = messages.find((m) => m.role === "system");
13774
+ const instructions = systemMsg ? this.extractTextContent(systemMsg) : "You are a helpful coding assistant.";
13775
+ const inputMessages = messages.filter((m) => m.role !== "system").map((msg) => this.convertMessagesToResponsesFormat([msg])[0]);
13776
+ const body = {
13777
+ model,
13778
+ instructions,
13779
+ input: inputMessages,
13780
+ tools: [],
13781
+ store: false,
13782
+ stream: true
13783
+ // Codex API requires streaming
13784
+ };
13785
+ const response = await this.makeRequest(body);
13786
+ if (!response.body) {
13787
+ throw new ProviderError("No response body from Codex API", {
13788
+ provider: this.id
13789
+ });
13790
+ }
13791
+ const reader = response.body.getReader();
13792
+ const decoder = new TextDecoder();
13793
+ let buffer = "";
13794
+ let content = "";
13795
+ let responseId = `codex-${Date.now()}`;
13796
+ let inputTokens = 0;
13797
+ let outputTokens = 0;
13798
+ let status = "completed";
13799
+ try {
13800
+ while (true) {
13801
+ const { done, value } = await reader.read();
13802
+ if (done) break;
13803
+ buffer += decoder.decode(value, { stream: true });
13804
+ const lines = buffer.split("\n");
13805
+ buffer = lines.pop() ?? "";
13806
+ for (const line of lines) {
13807
+ if (line.startsWith("data: ")) {
13808
+ const data = line.slice(6).trim();
13809
+ if (!data || data === "[DONE]") continue;
13810
+ try {
13811
+ const parsed = JSON.parse(data);
13812
+ if (parsed.id) {
13813
+ responseId = parsed.id;
13794
13814
  }
13795
- }
13796
- break;
13797
- case "response.completed":
13798
- {
13799
- for (const [, builder] of fnCallBuilders) {
13800
- yield {
13801
- type: "tool_use_end",
13802
- toolCall: {
13803
- id: builder.callId,
13804
- name: builder.name,
13805
- input: this.parseToolArguments(builder.arguments)
13806
- }
13807
- };
13815
+ if (parsed.type === "response.output_text.delta" && parsed.delta) {
13816
+ content += parsed.delta;
13817
+ } else if (parsed.type === "response.completed" && parsed.response) {
13818
+ if (parsed.response.usage) {
13819
+ inputTokens = parsed.response.usage.input_tokens ?? 0;
13820
+ outputTokens = parsed.response.usage.output_tokens ?? 0;
13821
+ }
13822
+ status = parsed.response.status ?? "completed";
13823
+ } else if (parsed.type === "response.output_text.done" && parsed.text) {
13824
+ content = parsed.text;
13808
13825
  }
13809
- fnCallBuilders.clear();
13810
- const hasToolCalls = event.response.output.some((i) => i.type === "function_call");
13811
- yield {
13812
- type: "done",
13813
- stopReason: hasToolCalls ? "tool_use" : "end_turn"
13814
- };
13826
+ } catch {
13815
13827
  }
13816
- break;
13828
+ }
13817
13829
  }
13818
13830
  }
13819
- } catch (error) {
13820
- throw this.handleError(error);
13831
+ } finally {
13832
+ reader.releaseLock();
13833
+ }
13834
+ if (!content) {
13835
+ throw new ProviderError("No response content from Codex API", {
13836
+ provider: this.id
13837
+ });
13821
13838
  }
13839
+ const stopReason = status === "completed" ? "end_turn" : status === "incomplete" ? "max_tokens" : "end_turn";
13840
+ return {
13841
+ id: responseId,
13842
+ content,
13843
+ stopReason,
13844
+ model,
13845
+ usage: {
13846
+ inputTokens,
13847
+ outputTokens
13848
+ }
13849
+ };
13822
13850
  }
13823
- // --- Responses API helpers ---
13824
13851
  /**
13825
- * Convert our internal messages to Responses API input format.
13826
- *
13827
- * The Responses API uses a flat array of input items (EasyInputMessage,
13828
- * function_call, function_call_output) instead of the chat completions
13829
- * messages array.
13852
+ * Send a chat message with tool use
13853
+ * Note: Codex Responses API tool support is complex; for now we delegate to chat()
13854
+ * and return empty toolCalls. Full tool support can be added later.
13830
13855
  */
13831
- convertToResponsesInput(messages, systemPrompt) {
13832
- const input = [];
13833
- let instructions = null;
13834
- if (systemPrompt) {
13835
- instructions = systemPrompt;
13836
- }
13837
- for (const msg of messages) {
13838
- if (msg.role === "system") {
13839
- instructions = (instructions ? instructions + "\n\n" : "") + this.contentToStr(msg.content);
13840
- } else if (msg.role === "user") {
13841
- if (Array.isArray(msg.content) && msg.content.some((b) => b.type === "tool_result")) {
13842
- for (const block of msg.content) {
13843
- if (block.type === "tool_result") {
13844
- const tr = block;
13845
- input.push({
13846
- type: "function_call_output",
13847
- call_id: tr.tool_use_id,
13848
- output: tr.content
13849
- });
13850
- }
13851
- }
13852
- } else {
13853
- input.push({
13854
- role: "user",
13855
- content: this.contentToStr(msg.content)
13856
- });
13857
- }
13858
- } else if (msg.role === "assistant") {
13859
- if (typeof msg.content === "string") {
13860
- input.push({
13861
- role: "assistant",
13862
- content: msg.content
13863
- });
13864
- } else if (Array.isArray(msg.content)) {
13865
- const textParts = [];
13866
- for (const block of msg.content) {
13867
- if (block.type === "text") {
13868
- textParts.push(block.text);
13869
- } else if (block.type === "tool_use") {
13870
- if (textParts.length > 0) {
13871
- input.push({
13872
- role: "assistant",
13873
- content: textParts.join("")
13874
- });
13875
- textParts.length = 0;
13876
- }
13877
- input.push({
13878
- type: "function_call",
13879
- call_id: block.id,
13880
- name: block.name,
13881
- arguments: JSON.stringify(block.input)
13882
- });
13883
- }
13884
- }
13885
- if (textParts.length > 0) {
13886
- input.push({
13887
- role: "assistant",
13888
- content: textParts.join("")
13889
- });
13890
- }
13856
+ async chatWithTools(messages, options) {
13857
+ const response = await this.chat(messages, options);
13858
+ return {
13859
+ ...response,
13860
+ toolCalls: []
13861
+ // Tools not yet supported in Codex provider
13862
+ };
13863
+ }
13864
+ /**
13865
+ * Stream a chat response
13866
+ * Note: True streaming with Codex Responses API is complex.
13867
+ * For now, we make a non-streaming call and simulate streaming by emitting chunks.
13868
+ */
13869
+ async *stream(messages, options) {
13870
+ const response = await this.chat(messages, options);
13871
+ if (response.content) {
13872
+ const content = response.content;
13873
+ const chunkSize = 20;
13874
+ for (let i = 0; i < content.length; i += chunkSize) {
13875
+ const chunk = content.slice(i, i + chunkSize);
13876
+ yield { type: "text", text: chunk };
13877
+ if (i + chunkSize < content.length) {
13878
+ await new Promise((resolve3) => setTimeout(resolve3, 5));
13891
13879
  }
13892
13880
  }
13893
13881
  }
13894
- return { input, instructions };
13882
+ yield { type: "done", stopReason: response.stopReason };
13895
13883
  }
13896
13884
  /**
13897
- * Convert our tool definitions to Responses API FunctionTool format
13885
+ * Stream a chat response with tool use
13886
+ * Note: Tools and true streaming with Codex Responses API are not yet implemented.
13887
+ * For now, we delegate to stream() which uses non-streaming under the hood.
13898
13888
  */
13899
- convertToolsForResponses(tools) {
13900
- return tools.map((tool) => ({
13901
- type: "function",
13902
- name: tool.name,
13903
- description: tool.description ?? void 0,
13904
- parameters: tool.input_schema ?? null,
13905
- strict: false
13906
- }));
13889
+ async *streamWithTools(messages, options) {
13890
+ yield* this.stream(messages, options);
13891
+ }
13892
+ };
13893
+ var CONTEXT_WINDOWS4 = {
13894
+ // Claude models
13895
+ "claude-sonnet-4.6": 2e5,
13896
+ "claude-opus-4.6": 2e5,
13897
+ "claude-sonnet-4.5": 2e5,
13898
+ "claude-opus-4.5": 2e5,
13899
+ "claude-haiku-4.5": 2e5,
13900
+ // OpenAI models — chat/completions
13901
+ "gpt-4.1": 1048576,
13902
+ // OpenAI models — /responses API (Codex/GPT-5+)
13903
+ "gpt-5.4-codex": 4e5,
13904
+ "gpt-5.3-codex": 4e5,
13905
+ "gpt-5.2-codex": 4e5,
13906
+ "gpt-5.1-codex-max": 4e5,
13907
+ "gpt-5.2": 4e5,
13908
+ "gpt-5.1": 4e5,
13909
+ // Google models
13910
+ "gemini-3.1-pro-preview": 1e6,
13911
+ "gemini-3-flash-preview": 1e6,
13912
+ "gemini-2.5-pro": 1048576
13913
+ };
13914
+ var DEFAULT_MODEL4 = "claude-sonnet-4.6";
13915
+ var COPILOT_HEADERS = {
13916
+ "Copilot-Integration-Id": "vscode-chat",
13917
+ "Editor-Version": "vscode/1.99.0",
13918
+ "Editor-Plugin-Version": "copilot-chat/0.26.7",
13919
+ "X-GitHub-Api-Version": "2025-04-01"
13920
+ };
13921
+ var CopilotProvider = class extends OpenAIProvider {
13922
+ baseUrl = "https://api.githubcopilot.com";
13923
+ currentToken = null;
13924
+ /** In-flight refresh promise to prevent concurrent token exchanges */
13925
+ refreshPromise = null;
13926
+ constructor() {
13927
+ super("copilot", "GitHub Copilot");
13907
13928
  }
13908
13929
  /**
13909
- * Parse tool call arguments with jsonrepair fallback
13930
+ * Initialize the provider with Copilot credentials.
13931
+ *
13932
+ * Gets a valid Copilot API token (from cache or by refreshing),
13933
+ * then creates an OpenAI client configured for the Copilot endpoint.
13910
13934
  */
13911
- parseToolArguments(args) {
13912
- try {
13913
- return args ? JSON.parse(args) : {};
13914
- } catch {
13915
- try {
13916
- if (args) {
13917
- const repaired = jsonrepair(args);
13918
- return JSON.parse(repaired);
13919
- }
13920
- } catch {
13921
- console.error(`[${this.name}] Cannot parse tool arguments: ${args.slice(0, 200)}`);
13922
- }
13923
- return {};
13935
+ async initialize(config) {
13936
+ this.config = {
13937
+ ...config,
13938
+ model: config.model ?? DEFAULT_MODEL4
13939
+ };
13940
+ const tokenResult = await getValidCopilotToken();
13941
+ if (tokenResult) {
13942
+ this.currentToken = tokenResult.token;
13943
+ this.baseUrl = tokenResult.baseUrl;
13944
+ } else if (config.apiKey) {
13945
+ this.currentToken = config.apiKey;
13946
+ }
13947
+ if (!this.currentToken) {
13948
+ throw new ProviderError(
13949
+ "No Copilot token found. Please authenticate with: coco --provider copilot",
13950
+ { provider: this.id }
13951
+ );
13924
13952
  }
13953
+ this.createCopilotClient();
13925
13954
  }
13926
13955
  /**
13927
- * Convert message content to string
13956
+ * Create the OpenAI client configured for Copilot API
13957
+ */
13958
+ createCopilotClient() {
13959
+ this.client = new OpenAI({
13960
+ apiKey: this.currentToken,
13961
+ baseURL: this.config.baseUrl ?? this.baseUrl,
13962
+ timeout: this.config.timeout ?? 12e4,
13963
+ defaultHeaders: COPILOT_HEADERS
13964
+ });
13965
+ }
13966
+ /**
13967
+ * Refresh the Copilot token if expired.
13968
+ *
13969
+ * Uses a mutex so concurrent callers share a single in-flight token
13970
+ * exchange. The slot is cleared inside the IIFE's finally block,
13971
+ * which runs after all awaiting callers have resumed.
13928
13972
  */
13929
- contentToStr(content) {
13930
- if (typeof content === "string") return content;
13931
- return content.filter((b) => b.type === "text").map((b) => b.text).join("");
13973
+ async refreshTokenIfNeeded() {
13974
+ if (!this.refreshPromise) {
13975
+ this.refreshPromise = (async () => {
13976
+ try {
13977
+ const tokenResult = await getValidCopilotToken();
13978
+ if (tokenResult && tokenResult.isNew) {
13979
+ this.currentToken = tokenResult.token;
13980
+ this.baseUrl = tokenResult.baseUrl;
13981
+ this.createCopilotClient();
13982
+ }
13983
+ } finally {
13984
+ this.refreshPromise = null;
13985
+ }
13986
+ })();
13987
+ }
13988
+ await this.refreshPromise;
13989
+ }
13990
+ // --- Override public methods to add token refresh ---
13991
+ async chat(messages, options) {
13992
+ await this.refreshTokenIfNeeded();
13993
+ return super.chat(messages, options);
13994
+ }
13995
+ async chatWithTools(messages, options) {
13996
+ await this.refreshTokenIfNeeded();
13997
+ return super.chatWithTools(messages, options);
13998
+ }
13999
+ async *stream(messages, options) {
14000
+ await this.refreshTokenIfNeeded();
14001
+ yield* super.stream(messages, options);
14002
+ }
14003
+ async *streamWithTools(messages, options) {
14004
+ await this.refreshTokenIfNeeded();
14005
+ yield* super.streamWithTools(messages, options);
13932
14006
  }
13933
14007
  // --- Override metadata methods ---
13934
14008
  /**