@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/cli/index.js +2175 -2080
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +998 -924
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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
|
|
11808
|
+
const timeoutController = new AbortController();
|
|
11809
|
+
const timeoutInterval = setInterval(() => {
|
|
11809
11810
|
if (Date.now() - lastActivityTime > streamTimeout) {
|
|
11810
|
-
|
|
11811
|
+
clearInterval(timeoutInterval);
|
|
11812
|
+
timeoutController.abort();
|
|
11811
11813
|
}
|
|
11812
|
-
};
|
|
11813
|
-
|
|
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
|
|
11867
|
+
const timeoutController = new AbortController();
|
|
11868
|
+
const timeoutInterval = setInterval(() => {
|
|
11861
11869
|
if (Date.now() - lastActivityTime > streamTimeout) {
|
|
11862
|
-
|
|
11870
|
+
clearInterval(timeoutInterval);
|
|
11871
|
+
timeoutController.abort();
|
|
11863
11872
|
}
|
|
11864
|
-
};
|
|
11865
|
-
|
|
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.
|
|
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.
|
|
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
|
|
12519
|
+
const timeoutController = new AbortController();
|
|
12520
|
+
const timeoutInterval = setInterval(() => {
|
|
12488
12521
|
if (Date.now() - lastActivityTime > streamTimeout) {
|
|
12489
|
-
|
|
12522
|
+
clearInterval(timeoutInterval);
|
|
12523
|
+
timeoutController.abort();
|
|
12490
12524
|
}
|
|
12491
|
-
};
|
|
12492
|
-
|
|
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
|
-
*
|
|
12946
|
-
* Uses the official Codex client ID (same as OpenCode, Codex CLI, etc.)
|
|
12969
|
+
* Simple chat via Responses API (no tools)
|
|
12947
12970
|
*/
|
|
12948
|
-
|
|
12949
|
-
|
|
12950
|
-
|
|
12951
|
-
|
|
12952
|
-
|
|
12953
|
-
|
|
12954
|
-
|
|
12955
|
-
|
|
12956
|
-
|
|
12957
|
-
|
|
12958
|
-
|
|
12959
|
-
|
|
12960
|
-
|
|
12961
|
-
|
|
12962
|
-
|
|
12963
|
-
|
|
12964
|
-
|
|
12965
|
-
|
|
12966
|
-
|
|
12967
|
-
|
|
12968
|
-
|
|
12969
|
-
|
|
12970
|
-
|
|
12971
|
-
|
|
12972
|
-
|
|
12973
|
-
|
|
12974
|
-
|
|
12975
|
-
|
|
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
|
-
|
|
13014
|
-
|
|
13015
|
-
|
|
13016
|
-
|
|
13017
|
-
|
|
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
|
-
|
|
13021
|
-
|
|
13022
|
-
|
|
13023
|
-
|
|
13024
|
-
|
|
13025
|
-
|
|
13026
|
-
|
|
13027
|
-
|
|
13028
|
-
|
|
13029
|
-
|
|
13030
|
-
|
|
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
|
-
|
|
13033
|
-
|
|
13034
|
-
|
|
13035
|
-
|
|
13036
|
-
|
|
13037
|
-
|
|
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
|
-
|
|
13061
|
-
|
|
13062
|
-
|
|
13063
|
-
|
|
13064
|
-
|
|
13065
|
-
|
|
13066
|
-
|
|
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
|
|
13070
|
-
const
|
|
13071
|
-
|
|
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
|
-
|
|
13074
|
-
|
|
13075
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
13402
|
+
function getTokenStoragePath(provider) {
|
|
13103
13403
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
13104
|
-
return path17.join(home, ".coco", "tokens",
|
|
13404
|
+
return path17.join(home, ".coco", "tokens", `${provider}.json`);
|
|
13105
13405
|
}
|
|
13106
|
-
async function
|
|
13107
|
-
const filePath =
|
|
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(
|
|
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
|
|
13412
|
+
async function loadTokens(provider) {
|
|
13413
|
+
const filePath = getTokenStoragePath(provider);
|
|
13128
13414
|
try {
|
|
13129
|
-
await fs16.
|
|
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
|
-
|
|
13197
|
-
|
|
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
|
-
|
|
13424
|
+
await fs16.unlink(filePath);
|
|
13222
13425
|
} catch {
|
|
13223
|
-
return void 0;
|
|
13224
13426
|
}
|
|
13225
13427
|
}
|
|
13226
|
-
function
|
|
13227
|
-
|
|
13228
|
-
|
|
13229
|
-
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
13233
|
-
|
|
13234
|
-
|
|
13235
|
-
|
|
13236
|
-
|
|
13237
|
-
|
|
13238
|
-
|
|
13239
|
-
|
|
13240
|
-
|
|
13241
|
-
|
|
13242
|
-
|
|
13243
|
-
|
|
13244
|
-
|
|
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
|
-
|
|
13270
|
-
|
|
13271
|
-
|
|
13272
|
-
|
|
13273
|
-
return
|
|
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
|
-
|
|
13277
|
-
|
|
13278
|
-
|
|
13279
|
-
|
|
13280
|
-
|
|
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
|
-
|
|
13284
|
-
|
|
13285
|
-
|
|
13286
|
-
|
|
13287
|
-
|
|
13288
|
-
|
|
13289
|
-
|
|
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
|
-
|
|
13295
|
-
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
|
|
13299
|
-
|
|
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
|
-
|
|
13306
|
-
|
|
13307
|
-
|
|
13308
|
-
|
|
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
|
-
|
|
13500
|
+
throw new Error(`Copilot token exchange failed: ${response.status} - ${error}`);
|
|
13318
13501
|
}
|
|
13319
|
-
|
|
13320
|
-
|
|
13321
|
-
|
|
13322
|
-
|
|
13323
|
-
|
|
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
|
-
|
|
13337
|
-
|
|
13338
|
-
|
|
13339
|
-
|
|
13340
|
-
|
|
13341
|
-
|
|
13342
|
-
|
|
13343
|
-
|
|
13344
|
-
|
|
13345
|
-
|
|
13346
|
-
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13352
|
-
|
|
13353
|
-
|
|
13354
|
-
|
|
13355
|
-
|
|
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
|
-
|
|
13361
|
-
|
|
13362
|
-
|
|
13363
|
-
|
|
13364
|
-
|
|
13365
|
-
|
|
13366
|
-
|
|
13367
|
-
|
|
13368
|
-
|
|
13369
|
-
|
|
13370
|
-
|
|
13371
|
-
|
|
13372
|
-
|
|
13373
|
-
|
|
13374
|
-
|
|
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
|
-
|
|
13433
|
-
|
|
13434
|
-
|
|
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
|
-
|
|
13444
|
-
|
|
13445
|
-
|
|
13446
|
-
|
|
13447
|
-
|
|
13448
|
-
|
|
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
|
-
|
|
13451
|
-
|
|
13452
|
-
|
|
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
|
-
|
|
13457
|
-
|
|
13458
|
-
|
|
13459
|
-
|
|
13460
|
-
|
|
13461
|
-
|
|
13462
|
-
|
|
13463
|
-
|
|
13464
|
-
|
|
13465
|
-
|
|
13466
|
-
|
|
13467
|
-
|
|
13468
|
-
|
|
13469
|
-
|
|
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
|
-
|
|
13601
|
+
return null;
|
|
13474
13602
|
}
|
|
13475
|
-
|
|
13476
|
-
|
|
13477
|
-
|
|
13478
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13515
|
-
|
|
13516
|
-
|
|
13517
|
-
|
|
13518
|
-
|
|
13519
|
-
|
|
13520
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
13536
|
-
this.
|
|
13654
|
+
this.accessToken = tokenResult.accessToken;
|
|
13655
|
+
this.accountId = extractAccountId(tokenResult.accessToken);
|
|
13537
13656
|
} else if (config.apiKey) {
|
|
13538
|
-
this.
|
|
13657
|
+
this.accessToken = config.apiKey;
|
|
13658
|
+
this.accountId = extractAccountId(config.apiKey);
|
|
13539
13659
|
}
|
|
13540
|
-
if (!this.
|
|
13660
|
+
if (!this.accessToken) {
|
|
13541
13661
|
throw new ProviderError(
|
|
13542
|
-
"No
|
|
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
|
-
|
|
13613
|
-
|
|
13614
|
-
|
|
13615
|
-
|
|
13616
|
-
|
|
13617
|
-
|
|
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
|
-
*
|
|
13678
|
+
* Get context window size for a model
|
|
13624
13679
|
*/
|
|
13625
|
-
|
|
13626
|
-
this.
|
|
13627
|
-
return
|
|
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
|
-
*
|
|
13685
|
+
* Count tokens in text (approximate)
|
|
13686
|
+
* Uses GPT-4 approximation: ~4 chars per token
|
|
13656
13687
|
*/
|
|
13657
|
-
|
|
13658
|
-
|
|
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
|
-
*
|
|
13692
|
+
* Check if provider is available (has valid OAuth tokens)
|
|
13708
13693
|
*/
|
|
13709
|
-
async
|
|
13710
|
-
this.ensureInitialized();
|
|
13694
|
+
async isAvailable() {
|
|
13711
13695
|
try {
|
|
13712
|
-
const
|
|
13713
|
-
|
|
13714
|
-
|
|
13715
|
-
|
|
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
|
-
*
|
|
13703
|
+
* Make a request to the Codex API
|
|
13736
13704
|
*/
|
|
13737
|
-
async
|
|
13705
|
+
async makeRequest(body) {
|
|
13738
13706
|
this.ensureInitialized();
|
|
13739
|
-
|
|
13740
|
-
|
|
13741
|
-
|
|
13742
|
-
|
|
13743
|
-
|
|
13744
|
-
|
|
13745
|
-
|
|
13746
|
-
|
|
13747
|
-
|
|
13748
|
-
|
|
13749
|
-
|
|
13750
|
-
|
|
13751
|
-
|
|
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
|
-
|
|
13754
|
-
|
|
13755
|
-
|
|
13756
|
-
|
|
13757
|
-
|
|
13758
|
-
|
|
13759
|
-
|
|
13760
|
-
|
|
13761
|
-
|
|
13762
|
-
|
|
13763
|
-
|
|
13764
|
-
|
|
13765
|
-
|
|
13766
|
-
|
|
13767
|
-
|
|
13768
|
-
|
|
13769
|
-
|
|
13770
|
-
|
|
13771
|
-
|
|
13772
|
-
|
|
13773
|
-
|
|
13774
|
-
|
|
13775
|
-
|
|
13776
|
-
|
|
13777
|
-
|
|
13778
|
-
|
|
13779
|
-
|
|
13780
|
-
|
|
13781
|
-
|
|
13782
|
-
|
|
13783
|
-
|
|
13784
|
-
|
|
13785
|
-
|
|
13786
|
-
|
|
13787
|
-
|
|
13788
|
-
|
|
13789
|
-
|
|
13790
|
-
|
|
13791
|
-
|
|
13792
|
-
|
|
13793
|
-
|
|
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
|
-
|
|
13797
|
-
|
|
13798
|
-
|
|
13799
|
-
|
|
13800
|
-
|
|
13801
|
-
|
|
13802
|
-
|
|
13803
|
-
|
|
13804
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13828
|
+
}
|
|
13817
13829
|
}
|
|
13818
13830
|
}
|
|
13819
|
-
}
|
|
13820
|
-
|
|
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
|
-
*
|
|
13826
|
-
*
|
|
13827
|
-
*
|
|
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
|
-
|
|
13832
|
-
const
|
|
13833
|
-
|
|
13834
|
-
|
|
13835
|
-
|
|
13836
|
-
|
|
13837
|
-
|
|
13838
|
-
|
|
13839
|
-
|
|
13840
|
-
|
|
13841
|
-
|
|
13842
|
-
|
|
13843
|
-
|
|
13844
|
-
|
|
13845
|
-
|
|
13846
|
-
|
|
13847
|
-
|
|
13848
|
-
|
|
13849
|
-
|
|
13850
|
-
|
|
13851
|
-
|
|
13852
|
-
|
|
13853
|
-
|
|
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
|
-
|
|
13882
|
+
yield { type: "done", stopReason: response.stopReason };
|
|
13895
13883
|
}
|
|
13896
13884
|
/**
|
|
13897
|
-
*
|
|
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
|
-
|
|
13900
|
-
|
|
13901
|
-
|
|
13902
|
-
|
|
13903
|
-
|
|
13904
|
-
|
|
13905
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
13912
|
-
|
|
13913
|
-
|
|
13914
|
-
|
|
13915
|
-
|
|
13916
|
-
|
|
13917
|
-
|
|
13918
|
-
|
|
13919
|
-
|
|
13920
|
-
|
|
13921
|
-
|
|
13922
|
-
|
|
13923
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
13930
|
-
if (
|
|
13931
|
-
|
|
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
|
/**
|