@corbat-tech/coco 2.11.0 → 2.11.1

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