@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/cli/index.js +2135 -2065
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +990 -919
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
}
|
|
@@ -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
|
|
12518
|
+
const timeoutController = new AbortController();
|
|
12519
|
+
const timeoutInterval = setInterval(() => {
|
|
12488
12520
|
if (Date.now() - lastActivityTime > streamTimeout) {
|
|
12489
|
-
|
|
12521
|
+
clearInterval(timeoutInterval);
|
|
12522
|
+
timeoutController.abort();
|
|
12490
12523
|
}
|
|
12491
|
-
};
|
|
12492
|
-
|
|
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
|
-
*
|
|
12946
|
-
* Uses the official Codex client ID (same as OpenCode, Codex CLI, etc.)
|
|
12968
|
+
* Simple chat via Responses API (no tools)
|
|
12947
12969
|
*/
|
|
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;
|
|
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
|
-
|
|
13014
|
-
|
|
13015
|
-
|
|
13016
|
-
|
|
13017
|
-
|
|
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
|
-
|
|
13021
|
-
|
|
13022
|
-
|
|
13023
|
-
|
|
13024
|
-
|
|
13025
|
-
|
|
13026
|
-
|
|
13027
|
-
|
|
13028
|
-
|
|
13029
|
-
|
|
13030
|
-
|
|
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
|
-
|
|
13033
|
-
|
|
13034
|
-
|
|
13035
|
-
|
|
13036
|
-
|
|
13037
|
-
|
|
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
|
-
|
|
13061
|
-
|
|
13062
|
-
|
|
13063
|
-
|
|
13064
|
-
|
|
13065
|
-
|
|
13066
|
-
|
|
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
|
|
13070
|
-
const
|
|
13071
|
-
|
|
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
|
-
|
|
13074
|
-
|
|
13075
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
13401
|
+
function getTokenStoragePath(provider) {
|
|
13103
13402
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
13104
|
-
return path17.join(home, ".coco", "tokens",
|
|
13403
|
+
return path17.join(home, ".coco", "tokens", `${provider}.json`);
|
|
13105
13404
|
}
|
|
13106
|
-
async function
|
|
13107
|
-
const filePath =
|
|
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(
|
|
13409
|
+
await fs16.writeFile(filePath, JSON.stringify(tokens, null, 2), { mode: 384 });
|
|
13111
13410
|
}
|
|
13112
|
-
|
|
13113
|
-
|
|
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(
|
|
13121
|
-
|
|
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
|
|
13128
|
-
|
|
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
|
-
|
|
13423
|
+
await fs16.unlink(filePath);
|
|
13222
13424
|
} catch {
|
|
13223
|
-
return void 0;
|
|
13224
13425
|
}
|
|
13225
13426
|
}
|
|
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
|
-
});
|
|
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
|
-
|
|
13270
|
-
|
|
13271
|
-
|
|
13272
|
-
|
|
13273
|
-
return
|
|
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
|
-
|
|
13277
|
-
|
|
13278
|
-
|
|
13279
|
-
|
|
13280
|
-
|
|
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
|
-
|
|
13284
|
-
|
|
13285
|
-
|
|
13286
|
-
|
|
13287
|
-
|
|
13288
|
-
|
|
13289
|
-
|
|
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
|
-
|
|
13295
|
-
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
|
|
13299
|
-
|
|
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
|
-
|
|
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
|
-
});
|
|
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
|
-
|
|
13499
|
+
throw new Error(`Copilot token exchange failed: ${response.status} - ${error}`);
|
|
13318
13500
|
}
|
|
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 "";
|
|
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
|
-
|
|
13337
|
-
|
|
13338
|
-
|
|
13339
|
-
|
|
13340
|
-
|
|
13341
|
-
|
|
13342
|
-
|
|
13343
|
-
|
|
13344
|
-
|
|
13345
|
-
|
|
13346
|
-
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13352
|
-
|
|
13353
|
-
|
|
13354
|
-
|
|
13355
|
-
|
|
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
|
-
|
|
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";
|
|
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
|
-
|
|
13433
|
-
|
|
13434
|
-
|
|
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
|
-
|
|
13444
|
-
|
|
13445
|
-
|
|
13446
|
-
|
|
13447
|
-
|
|
13448
|
-
|
|
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
|
-
|
|
13451
|
-
|
|
13452
|
-
|
|
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
|
-
|
|
13457
|
-
|
|
13458
|
-
|
|
13459
|
-
|
|
13460
|
-
|
|
13461
|
-
|
|
13462
|
-
|
|
13463
|
-
|
|
13464
|
-
|
|
13465
|
-
|
|
13466
|
-
|
|
13467
|
-
|
|
13468
|
-
|
|
13469
|
-
|
|
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
|
-
|
|
13600
|
+
return null;
|
|
13474
13601
|
}
|
|
13475
|
-
|
|
13476
|
-
|
|
13477
|
-
|
|
13478
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13515
|
-
|
|
13516
|
-
|
|
13517
|
-
|
|
13518
|
-
|
|
13519
|
-
|
|
13520
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
13536
|
-
this.
|
|
13652
|
+
this.accessToken = tokenResult.accessToken;
|
|
13653
|
+
this.accountId = extractAccountId(tokenResult.accessToken);
|
|
13537
13654
|
} else if (config.apiKey) {
|
|
13538
|
-
this.
|
|
13655
|
+
this.accessToken = config.apiKey;
|
|
13656
|
+
this.accountId = extractAccountId(config.apiKey);
|
|
13539
13657
|
}
|
|
13540
|
-
if (!this.
|
|
13658
|
+
if (!this.accessToken) {
|
|
13541
13659
|
throw new ProviderError(
|
|
13542
|
-
"No
|
|
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
|
-
|
|
13613
|
-
|
|
13614
|
-
|
|
13615
|
-
|
|
13616
|
-
|
|
13617
|
-
|
|
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
|
-
*
|
|
13676
|
+
* Get context window size for a model
|
|
13624
13677
|
*/
|
|
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);
|
|
13678
|
+
getContextWindow(model) {
|
|
13679
|
+
const m = model ?? this.config.model ?? DEFAULT_MODEL3;
|
|
13680
|
+
return CONTEXT_WINDOWS3[m] ?? 128e3;
|
|
13653
13681
|
}
|
|
13654
13682
|
/**
|
|
13655
|
-
*
|
|
13683
|
+
* Count tokens in text (approximate)
|
|
13684
|
+
* Uses GPT-4 approximation: ~4 chars per token
|
|
13656
13685
|
*/
|
|
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);
|
|
13686
|
+
countTokens(text) {
|
|
13687
|
+
return Math.ceil(text.length / 4);
|
|
13705
13688
|
}
|
|
13706
13689
|
/**
|
|
13707
|
-
*
|
|
13690
|
+
* Check if provider is available (has valid OAuth tokens)
|
|
13708
13691
|
*/
|
|
13709
|
-
async
|
|
13710
|
-
this.ensureInitialized();
|
|
13692
|
+
async isAvailable() {
|
|
13711
13693
|
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);
|
|
13694
|
+
const tokenResult = await getValidAccessToken("openai");
|
|
13695
|
+
return tokenResult !== null;
|
|
13696
|
+
} catch {
|
|
13697
|
+
return false;
|
|
13732
13698
|
}
|
|
13733
13699
|
}
|
|
13734
13700
|
/**
|
|
13735
|
-
*
|
|
13701
|
+
* Make a request to the Codex API
|
|
13736
13702
|
*/
|
|
13737
|
-
async
|
|
13703
|
+
async makeRequest(body) {
|
|
13738
13704
|
this.ensureInitialized();
|
|
13739
|
-
|
|
13740
|
-
|
|
13741
|
-
|
|
13742
|
-
|
|
13743
|
-
|
|
13744
|
-
|
|
13745
|
-
|
|
13746
|
-
|
|
13747
|
-
|
|
13748
|
-
|
|
13749
|
-
|
|
13750
|
-
|
|
13751
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13797
|
-
|
|
13798
|
-
|
|
13799
|
-
|
|
13800
|
-
|
|
13801
|
-
|
|
13802
|
-
|
|
13803
|
-
|
|
13804
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13826
|
+
}
|
|
13817
13827
|
}
|
|
13818
13828
|
}
|
|
13819
|
-
}
|
|
13820
|
-
|
|
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
|
-
*
|
|
13826
|
-
*
|
|
13827
|
-
*
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
13880
|
+
yield { type: "done", stopReason: response.stopReason };
|
|
13895
13881
|
}
|
|
13896
13882
|
/**
|
|
13897
|
-
*
|
|
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
|
-
|
|
13900
|
-
|
|
13901
|
-
|
|
13902
|
-
|
|
13903
|
-
|
|
13904
|
-
|
|
13905
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
13912
|
-
|
|
13913
|
-
|
|
13914
|
-
|
|
13915
|
-
|
|
13916
|
-
|
|
13917
|
-
|
|
13918
|
-
|
|
13919
|
-
|
|
13920
|
-
|
|
13921
|
-
|
|
13922
|
-
|
|
13923
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
13930
|
-
if (
|
|
13931
|
-
|
|
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
|
/**
|