@timo972/cc-router 0.11.0 → 0.12.0-rc.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/CHANGELOG.md +12 -0
- package/README.md +33 -4
- package/dist/cli/cmd-accounts.js +91 -13
- package/dist/cli/cmd-cli-targets.js +177 -0
- package/dist/cli/cmd-client.js +3 -2
- package/dist/cli/cmd-configure.js +18 -4
- package/dist/cli/cmd-status.js +4 -1
- package/dist/cli/cmd-stop.js +18 -0
- package/dist/cli/index.js +10 -3
- package/dist/config/manager.js +19 -3
- package/dist/config/paths.js +1 -0
- package/dist/protocol/anthropic-to-openai.js +31 -19
- package/dist/protocol/model-ref.js +1 -1
- package/dist/protocol/openai-function-call.js +16 -0
- package/dist/protocol/openai-response-to-anthropic.js +34 -6
- package/dist/protocol/openai-stream-to-anthropic.js +116 -16
- package/dist/protocol/openai-to-anthropic.js +54 -29
- package/dist/providers/openai/account-state.js +4 -1
- package/dist/providers/openai/usage.js +14 -1
- package/dist/providers/xai/account-record.js +42 -0
- package/dist/providers/xai/device-oauth.js +116 -0
- package/dist/providers/xai/import-auth.js +70 -0
- package/dist/providers/xai/overview.js +279 -0
- package/dist/providers/xai/subscription-fetch.js +56 -0
- package/dist/proxy/allowance.js +142 -0
- package/dist/proxy/logger.js +4 -3
- package/dist/proxy/messages-cross-route.js +113 -8
- package/dist/proxy/models-server.js +30 -2
- package/dist/proxy/openai-ingress.js +7 -1
- package/dist/proxy/openai-routing.js +12 -7
- package/dist/proxy/server.js +110 -6
- package/dist/ui/Dashboard.js +383 -124
- package/dist/ui/accountsApi.js +3 -1
- package/dist/utils/cli-routing.js +80 -0
- package/dist/utils/codex-config.js +337 -15
- package/package.json +3 -2
package/dist/config/manager.js
CHANGED
|
@@ -110,9 +110,11 @@ export function renameAccountRecordById(oldId, newId) {
|
|
|
110
110
|
return target;
|
|
111
111
|
}
|
|
112
112
|
function normalizeAccountProvider(record) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
if (record.provider === "openai_subscription")
|
|
114
|
+
return "openai_subscription";
|
|
115
|
+
if (record.provider === "xai_subscription")
|
|
116
|
+
return "xai_subscription";
|
|
117
|
+
return "anthropic_subscription";
|
|
116
118
|
}
|
|
117
119
|
export function migrateLegacyAccountProviders(path = ACCOUNTS_PATH) {
|
|
118
120
|
const records = readRawFromPath(path);
|
|
@@ -184,6 +186,20 @@ export function saveOpenAIAccountsToPath(accounts, path) {
|
|
|
184
186
|
export function saveOpenAIAccounts(accounts) {
|
|
185
187
|
saveOpenAIAccountsToPath(accounts, ACCOUNTS_PATH);
|
|
186
188
|
}
|
|
189
|
+
export function loadXaiAccounts(path) {
|
|
190
|
+
const records = readRawFromPath(path ?? ACCOUNTS_PATH);
|
|
191
|
+
return records
|
|
192
|
+
.filter(a => a.provider === "xai_subscription")
|
|
193
|
+
.map(a => ({
|
|
194
|
+
id: a.id,
|
|
195
|
+
provider: "xai_subscription",
|
|
196
|
+
accessToken: a.accessToken,
|
|
197
|
+
refreshToken: a.refreshToken,
|
|
198
|
+
expiresAt: a.expiresAt,
|
|
199
|
+
enabled: a.enabled !== false,
|
|
200
|
+
...(Array.isArray(a.scopes) ? { scopes: a.scopes } : {}),
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
187
203
|
function parseProxyConfig(raw) {
|
|
188
204
|
const parsed = JSON.parse(raw);
|
|
189
205
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
package/dist/config/paths.js
CHANGED
|
@@ -5,6 +5,7 @@ export const CONFIG_DIR = path.join(os.homedir(), ".cc-router");
|
|
|
5
5
|
export const ACCOUNTS_PATH = process.env["ACCOUNTS_PATH"] ??
|
|
6
6
|
path.join(CONFIG_DIR, "accounts.json");
|
|
7
7
|
export const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
8
|
+
export const CODEX_CONFIG_PATH = path.join(os.homedir(), ".codex", "config.toml");
|
|
8
9
|
export const PROXY_PORT = parseInt(process.env["PORT"] ?? "3456", 10);
|
|
9
10
|
export const LITELLM_PORT = 4000;
|
|
10
11
|
// When set, the server forwards to LiteLLM instead of Anthropic directly
|
|
@@ -6,39 +6,51 @@ function stringifySystem(system) {
|
|
|
6
6
|
return system;
|
|
7
7
|
return system.map(block => block.text).join("\n");
|
|
8
8
|
}
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
function messageToOpenAIItems(role, content) {
|
|
10
|
+
const textType = role === "assistant" ? "output_text" : "input_text";
|
|
11
|
+
if (typeof content === "string") {
|
|
12
|
+
return [{ role, content: [{ type: textType, text: content }] }];
|
|
13
|
+
}
|
|
14
|
+
const items = [];
|
|
15
|
+
let pendingText = [];
|
|
16
|
+
const flushText = () => {
|
|
17
|
+
if (pendingText.length === 0)
|
|
18
|
+
return;
|
|
19
|
+
items.push({ role, content: pendingText });
|
|
20
|
+
pendingText = [];
|
|
21
|
+
};
|
|
22
|
+
for (const block of content) {
|
|
23
|
+
if (block.type === "text") {
|
|
24
|
+
pendingText.push({ type: textType, text: block.text });
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
flushText();
|
|
15
28
|
if (block.type === "tool_use") {
|
|
16
|
-
|
|
29
|
+
items.push({
|
|
17
30
|
type: "function_call",
|
|
18
31
|
call_id: block.id,
|
|
19
32
|
name: block.name,
|
|
20
33
|
arguments: JSON.stringify(block.input ?? {}),
|
|
21
|
-
};
|
|
34
|
+
});
|
|
35
|
+
continue;
|
|
22
36
|
}
|
|
23
|
-
|
|
24
|
-
? block.content
|
|
25
|
-
: block.content.map(item => item.text).join("\n");
|
|
26
|
-
return {
|
|
37
|
+
items.push({
|
|
27
38
|
type: "function_call_output",
|
|
28
39
|
call_id: block.tool_use_id,
|
|
29
|
-
output
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
output: typeof block.content === "string"
|
|
41
|
+
? block.content
|
|
42
|
+
: block.content.map(item => item.text).join("\n"),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
flushText();
|
|
46
|
+
return items;
|
|
32
47
|
}
|
|
33
48
|
export function anthropicToOpenAIResponses(req, modelRouting = {}) {
|
|
34
49
|
const parsed = parseModelRef(req.model, modelRouting);
|
|
35
50
|
return {
|
|
36
51
|
model: parsed.upstreamModel,
|
|
37
52
|
instructions: stringifySystem(req.system),
|
|
38
|
-
input: req.messages.
|
|
39
|
-
role: message.role,
|
|
40
|
-
content: contentToOpenAI(message.content),
|
|
41
|
-
})),
|
|
53
|
+
input: req.messages.flatMap(message => messageToOpenAIItems(message.role, message.content)),
|
|
42
54
|
tools: req.tools?.map(tool => ({
|
|
43
55
|
type: "function",
|
|
44
56
|
name: tool.name,
|
|
@@ -20,7 +20,7 @@ function cleanModel(model) {
|
|
|
20
20
|
* costs the Anthropic path nothing. Everything else unprefixed still goes to
|
|
21
21
|
* Anthropic, which is what existing setups rely on.
|
|
22
22
|
*/
|
|
23
|
-
function isBareOpenAIModel(publicModel) {
|
|
23
|
+
export function isBareOpenAIModel(publicModel) {
|
|
24
24
|
return publicModel.toLowerCase().startsWith("gpt-");
|
|
25
25
|
}
|
|
26
26
|
export function parseModelRef(model, config = {}) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class OpenAIProtocolError extends Error {
|
|
2
|
+
name = "OpenAIProtocolError";
|
|
3
|
+
}
|
|
4
|
+
export function parseOpenAIFunctionArguments(argumentsJson) {
|
|
5
|
+
let parsed;
|
|
6
|
+
try {
|
|
7
|
+
parsed = JSON.parse(argumentsJson);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
throw new OpenAIProtocolError("Invalid OpenAI function call arguments");
|
|
11
|
+
}
|
|
12
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
13
|
+
throw new OpenAIProtocolError("Invalid OpenAI function call arguments");
|
|
14
|
+
}
|
|
15
|
+
return parsed;
|
|
16
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OpenAIProtocolError, parseOpenAIFunctionArguments } from "./openai-function-call.js";
|
|
1
2
|
/**
|
|
2
3
|
* Anthropic stop reason for a terminal Responses payload. Shared by both
|
|
3
4
|
* translation paths — this module for a collected response, and the streaming
|
|
@@ -14,18 +15,45 @@ export function anthropicStopReasonForResponse(response) {
|
|
|
14
15
|
return response?.incomplete_details?.reason === "max_output_tokens" ? "max_tokens" : "end_turn";
|
|
15
16
|
}
|
|
16
17
|
export function openAIResponseToAnthropicMessage(response) {
|
|
17
|
-
const content =
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
const content = [];
|
|
19
|
+
let sawRefusal = false;
|
|
20
|
+
for (const item of response.output ?? []) {
|
|
21
|
+
if (item.type === "message") {
|
|
22
|
+
for (const part of item.content) {
|
|
23
|
+
if (part.type === "output_text") {
|
|
24
|
+
content.push({ type: "text", text: part.text });
|
|
25
|
+
}
|
|
26
|
+
else if (part.type === "refusal") {
|
|
27
|
+
sawRefusal = true;
|
|
28
|
+
content.push({ type: "text", text: part.refusal });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (item.type !== "function_call")
|
|
34
|
+
continue;
|
|
35
|
+
if (!item.call_id?.trim() || !item.name?.trim()) {
|
|
36
|
+
throw new OpenAIProtocolError("Invalid OpenAI function call metadata");
|
|
37
|
+
}
|
|
38
|
+
content.push({
|
|
39
|
+
type: "tool_use",
|
|
40
|
+
id: item.call_id,
|
|
41
|
+
name: item.name,
|
|
42
|
+
input: parseOpenAIFunctionArguments(item.arguments),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const stopReason = content.some(block => block.type === "tool_use")
|
|
46
|
+
? "tool_use"
|
|
47
|
+
: sawRefusal
|
|
48
|
+
? "refusal"
|
|
49
|
+
: anthropicStopReasonForResponse(response);
|
|
22
50
|
return {
|
|
23
51
|
id: response.id,
|
|
24
52
|
type: "message",
|
|
25
53
|
role: "assistant",
|
|
26
54
|
model: response.model ?? "",
|
|
27
55
|
content,
|
|
28
|
-
stop_reason:
|
|
56
|
+
stop_reason: stopReason,
|
|
29
57
|
stop_sequence: null,
|
|
30
58
|
usage: {
|
|
31
59
|
input_tokens: response.usage?.input_tokens ?? 0,
|
|
@@ -1,25 +1,39 @@
|
|
|
1
1
|
import { anthropicStopReasonForResponse } from "./openai-response-to-anthropic.js";
|
|
2
|
+
import { OpenAIProtocolError, parseOpenAIFunctionArguments } from "./openai-function-call.js";
|
|
2
3
|
import { terminalResponsePayload } from "./openai-responses-collect.js";
|
|
3
4
|
export function createOpenAIStreamToAnthropicNormalizer() {
|
|
4
|
-
let
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
let blocks = new Map();
|
|
6
|
+
let nextIndex = 0;
|
|
7
|
+
let sawToolUse = false;
|
|
8
|
+
let sawRefusal = false;
|
|
9
|
+
const openTextBlock = (outputIndex) => {
|
|
10
|
+
if (blocks.has(outputIndex))
|
|
7
11
|
return [];
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
const block = { index: nextIndex++, kind: "text", sentArguments: false, argumentsJson: "" };
|
|
13
|
+
blocks.set(outputIndex, block);
|
|
14
|
+
return [{
|
|
11
15
|
type: "content_block_start",
|
|
12
|
-
index:
|
|
16
|
+
index: block.index,
|
|
13
17
|
content_block: { type: "text", text: "" },
|
|
14
|
-
}
|
|
15
|
-
|
|
18
|
+
}];
|
|
19
|
+
};
|
|
20
|
+
const closeBlock = (outputIndex) => {
|
|
21
|
+
const block = blocks.get(outputIndex);
|
|
22
|
+
if (!block)
|
|
23
|
+
return [];
|
|
24
|
+
blocks.delete(outputIndex);
|
|
25
|
+
return [{ type: "content_block_stop", index: block.index }];
|
|
16
26
|
};
|
|
17
27
|
const reset = () => {
|
|
18
|
-
|
|
28
|
+
blocks = new Map();
|
|
29
|
+
nextIndex = 0;
|
|
30
|
+
sawToolUse = false;
|
|
31
|
+
sawRefusal = false;
|
|
19
32
|
};
|
|
20
33
|
return {
|
|
21
34
|
reset,
|
|
22
35
|
convert(event) {
|
|
36
|
+
const outputIndex = event.output_index ?? 0;
|
|
23
37
|
if (event.type === "response.created") {
|
|
24
38
|
reset();
|
|
25
39
|
return [
|
|
@@ -38,16 +52,96 @@ export function createOpenAIStreamToAnthropicNormalizer() {
|
|
|
38
52
|
},
|
|
39
53
|
];
|
|
40
54
|
}
|
|
55
|
+
if (event.type === "response.output_item.added") {
|
|
56
|
+
if (event.item?.type !== "function_call" || blocks.has(outputIndex))
|
|
57
|
+
return [];
|
|
58
|
+
if (!event.item.call_id?.trim() || !event.item.name?.trim()) {
|
|
59
|
+
throw new OpenAIProtocolError("Invalid OpenAI function call metadata");
|
|
60
|
+
}
|
|
61
|
+
const block = {
|
|
62
|
+
index: nextIndex++,
|
|
63
|
+
kind: "tool_use",
|
|
64
|
+
sentArguments: false,
|
|
65
|
+
argumentsJson: "",
|
|
66
|
+
};
|
|
67
|
+
blocks.set(outputIndex, block);
|
|
68
|
+
sawToolUse = true;
|
|
69
|
+
return [{
|
|
70
|
+
type: "content_block_start",
|
|
71
|
+
index: block.index,
|
|
72
|
+
content_block: {
|
|
73
|
+
type: "tool_use",
|
|
74
|
+
id: event.item.call_id,
|
|
75
|
+
name: event.item.name,
|
|
76
|
+
input: {},
|
|
77
|
+
},
|
|
78
|
+
}];
|
|
79
|
+
}
|
|
41
80
|
if (event.type === "response.output_text.delta") {
|
|
81
|
+
const prefix = openTextBlock(outputIndex);
|
|
82
|
+
const block = blocks.get(outputIndex);
|
|
42
83
|
return [
|
|
43
|
-
...
|
|
84
|
+
...prefix,
|
|
44
85
|
{
|
|
45
86
|
type: "content_block_delta",
|
|
46
|
-
index: 0,
|
|
87
|
+
index: block?.index ?? 0,
|
|
47
88
|
delta: { type: "text_delta", text: event.delta ?? "" },
|
|
48
89
|
},
|
|
49
90
|
];
|
|
50
91
|
}
|
|
92
|
+
if (event.type === "response.refusal.delta") {
|
|
93
|
+
sawRefusal = true;
|
|
94
|
+
const prefix = openTextBlock(outputIndex);
|
|
95
|
+
const block = blocks.get(outputIndex);
|
|
96
|
+
return [...prefix, {
|
|
97
|
+
type: "content_block_delta",
|
|
98
|
+
index: block?.index ?? 0,
|
|
99
|
+
delta: { type: "text_delta", text: event.delta ?? "" },
|
|
100
|
+
}];
|
|
101
|
+
}
|
|
102
|
+
if (event.type === "response.function_call_arguments.delta") {
|
|
103
|
+
const block = blocks.get(outputIndex);
|
|
104
|
+
if (!block || block.kind !== "tool_use")
|
|
105
|
+
return [];
|
|
106
|
+
block.sentArguments = true;
|
|
107
|
+
block.argumentsJson += event.delta ?? "";
|
|
108
|
+
return [{
|
|
109
|
+
type: "content_block_delta",
|
|
110
|
+
index: block.index,
|
|
111
|
+
delta: { type: "input_json_delta", partial_json: event.delta ?? "" },
|
|
112
|
+
}];
|
|
113
|
+
}
|
|
114
|
+
if (event.type === "response.function_call_arguments.done") {
|
|
115
|
+
const block = blocks.get(outputIndex);
|
|
116
|
+
if (!block || block.kind !== "tool_use" || block.sentArguments || !event.arguments)
|
|
117
|
+
return [];
|
|
118
|
+
block.sentArguments = true;
|
|
119
|
+
block.argumentsJson = event.arguments;
|
|
120
|
+
return [{
|
|
121
|
+
type: "content_block_delta",
|
|
122
|
+
index: block.index,
|
|
123
|
+
delta: { type: "input_json_delta", partial_json: event.arguments },
|
|
124
|
+
}];
|
|
125
|
+
}
|
|
126
|
+
if (event.type === "response.output_item.done") {
|
|
127
|
+
const block = blocks.get(outputIndex);
|
|
128
|
+
const atomicArguments = block?.kind === "tool_use" && !block.sentArguments
|
|
129
|
+
? event.item?.arguments ?? ""
|
|
130
|
+
: "";
|
|
131
|
+
if (block?.kind === "tool_use") {
|
|
132
|
+
if (atomicArguments)
|
|
133
|
+
block.argumentsJson = atomicArguments;
|
|
134
|
+
parseOpenAIFunctionArguments(block.argumentsJson);
|
|
135
|
+
}
|
|
136
|
+
const argumentEvent = block?.kind === "tool_use" && atomicArguments
|
|
137
|
+
? [{
|
|
138
|
+
type: "content_block_delta",
|
|
139
|
+
index: block.index,
|
|
140
|
+
delta: { type: "input_json_delta", partial_json: atomicArguments },
|
|
141
|
+
}]
|
|
142
|
+
: [];
|
|
143
|
+
return [...argumentEvent, ...closeBlock(outputIndex)];
|
|
144
|
+
}
|
|
51
145
|
// Both terminal Responses events must close the Anthropic message.
|
|
52
146
|
// Emitting nothing for `response.incomplete` would end the HTTP stream
|
|
53
147
|
// without `message_stop`, leaving the client waiting on a turn that is
|
|
@@ -63,10 +157,16 @@ export function createOpenAIStreamToAnthropicNormalizer() {
|
|
|
63
157
|
// a stream that never reached a terminal event looks like, and what
|
|
64
158
|
// clients already detect and surface as an error.
|
|
65
159
|
if (terminalResponsePayload(event) !== undefined) {
|
|
160
|
+
if ([...blocks.values()].some(block => block.kind === "tool_use")) {
|
|
161
|
+
throw new OpenAIProtocolError("OpenAI function call ended before completion");
|
|
162
|
+
}
|
|
66
163
|
const usage = event.response?.usage ?? {};
|
|
67
|
-
const prefix =
|
|
68
|
-
|
|
69
|
-
|
|
164
|
+
const prefix = [...blocks.keys()].flatMap(closeBlock);
|
|
165
|
+
const stopReason = sawToolUse
|
|
166
|
+
? "tool_use"
|
|
167
|
+
: sawRefusal
|
|
168
|
+
? "refusal"
|
|
169
|
+
: anthropicStopReasonForResponse(event.response);
|
|
70
170
|
reset();
|
|
71
171
|
return [
|
|
72
172
|
...prefix,
|
|
@@ -74,7 +174,7 @@ export function createOpenAIStreamToAnthropicNormalizer() {
|
|
|
74
174
|
type: "message_delta",
|
|
75
175
|
// Same helper the collected-response translator uses, so an
|
|
76
176
|
// incomplete turn reports the same stop reason on both paths.
|
|
77
|
-
delta: { stop_reason:
|
|
177
|
+
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
78
178
|
usage: { output_tokens: usage.output_tokens ?? 0 },
|
|
79
179
|
},
|
|
80
180
|
{ type: "message_stop" },
|
|
@@ -8,48 +8,73 @@ function parseArguments(args) {
|
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
function textFromOpenAI(block) {
|
|
11
|
-
|
|
12
|
-
return block.text;
|
|
13
|
-
return null;
|
|
11
|
+
return block.text;
|
|
14
12
|
}
|
|
15
13
|
function messageContentToAnthropic(message) {
|
|
16
|
-
const blocks = message.content.map((block) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
type: "tool_use",
|
|
23
|
-
id: block.call_id,
|
|
24
|
-
name: block.name,
|
|
25
|
-
input: parseArguments(block.arguments),
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
if (block.type === "function_call_output") {
|
|
29
|
-
return {
|
|
30
|
-
type: "tool_result",
|
|
31
|
-
tool_use_id: block.call_id,
|
|
32
|
-
content: block.output,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
}).filter((block) => block !== null);
|
|
37
|
-
if (blocks.length === 1 && blocks[0].type === "text")
|
|
14
|
+
const blocks = message.content.map((block) => ({
|
|
15
|
+
type: "text",
|
|
16
|
+
text: textFromOpenAI(block),
|
|
17
|
+
}));
|
|
18
|
+
if (blocks.length === 1)
|
|
38
19
|
return blocks[0].text;
|
|
39
20
|
return blocks;
|
|
40
21
|
}
|
|
41
22
|
function normalizeRole(role) {
|
|
42
23
|
return role === "assistant" ? "assistant" : "user";
|
|
43
24
|
}
|
|
25
|
+
function isFunctionCall(item) {
|
|
26
|
+
return "type" in item && item.type === "function_call";
|
|
27
|
+
}
|
|
28
|
+
function isFunctionCallOutput(item) {
|
|
29
|
+
return "type" in item && item.type === "function_call_output";
|
|
30
|
+
}
|
|
31
|
+
function inputItemsToAnthropicMessages(input) {
|
|
32
|
+
const messages = [];
|
|
33
|
+
const append = (role, blocks) => {
|
|
34
|
+
if (blocks.length === 0)
|
|
35
|
+
return;
|
|
36
|
+
const last = messages.at(-1);
|
|
37
|
+
if (last?.role === role) {
|
|
38
|
+
const existing = typeof last.content === "string"
|
|
39
|
+
? [{ type: "text", text: last.content }]
|
|
40
|
+
: last.content;
|
|
41
|
+
last.content = [...existing, ...blocks];
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
messages.push({ role, content: blocks });
|
|
45
|
+
};
|
|
46
|
+
for (const item of input) {
|
|
47
|
+
if (isFunctionCall(item)) {
|
|
48
|
+
append("assistant", [{
|
|
49
|
+
type: "tool_use",
|
|
50
|
+
id: item.call_id,
|
|
51
|
+
name: item.name,
|
|
52
|
+
input: parseArguments(item.arguments),
|
|
53
|
+
}]);
|
|
54
|
+
}
|
|
55
|
+
else if (isFunctionCallOutput(item)) {
|
|
56
|
+
append("user", [{ type: "tool_result", tool_use_id: item.call_id, content: item.output }]);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const content = messageContentToAnthropic(item);
|
|
60
|
+
append(normalizeRole(item.role), typeof content === "string"
|
|
61
|
+
? [{ type: "text", text: content }]
|
|
62
|
+
: content);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return messages.map(message => {
|
|
66
|
+
if (Array.isArray(message.content) && message.content.length === 1 && message.content[0].type === "text") {
|
|
67
|
+
return { ...message, content: message.content[0].text };
|
|
68
|
+
}
|
|
69
|
+
return message;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
44
72
|
export function openAIResponsesToAnthropic(req) {
|
|
45
73
|
const parsed = parseModelRef(req.model);
|
|
46
74
|
return {
|
|
47
75
|
model: parsed.upstreamModel,
|
|
48
76
|
system: req.instructions,
|
|
49
|
-
messages: req.input
|
|
50
|
-
role: normalizeRole(message.role),
|
|
51
|
-
content: messageContentToAnthropic(message),
|
|
52
|
-
})),
|
|
77
|
+
messages: inputItemsToAnthropicMessages(req.input),
|
|
53
78
|
tools: req.tools?.map(tool => ({
|
|
54
79
|
name: tool.name,
|
|
55
80
|
description: tool.description,
|
|
@@ -132,8 +132,11 @@ export function applyCodexRateLimits(account, update, nowMs) {
|
|
|
132
132
|
}
|
|
133
133
|
if (update.credits)
|
|
134
134
|
limits.credits = update.credits;
|
|
135
|
-
if (update.
|
|
135
|
+
if (update.resetCredits)
|
|
136
|
+
limits.resetCredits = update.resetCredits;
|
|
137
|
+
if (update.buckets.length > 0 || update.credits || update.resetCredits) {
|
|
136
138
|
limits.lastUpdated = nowMs;
|
|
139
|
+
}
|
|
137
140
|
}
|
|
138
141
|
function normalizeModelSlug(model) {
|
|
139
142
|
const normalized = model?.trim().toLowerCase();
|
|
@@ -246,7 +246,20 @@ export function parseCodexUsagePayload(value, nowMs) {
|
|
|
246
246
|
};
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
|
-
|
|
249
|
+
let resetCredits;
|
|
250
|
+
const rawResetCredits = payload["rate_limit_reset_credits"];
|
|
251
|
+
if (typeof rawResetCredits === "object" && rawResetCredits !== null) {
|
|
252
|
+
const record = rawResetCredits;
|
|
253
|
+
const count = usageNumber(record["available_count"]) ?? usageNumber(record["available"]);
|
|
254
|
+
if (count !== undefined) {
|
|
255
|
+
resetCredits = { available: Math.max(0, Math.min(99, Math.floor(count))) };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
buckets,
|
|
260
|
+
...(credits ? { credits } : {}),
|
|
261
|
+
...(resetCredits ? { resetCredits } : {}),
|
|
262
|
+
};
|
|
250
263
|
}
|
|
251
264
|
export function resolveActiveLimit(headers) {
|
|
252
265
|
const raw = headerString(headers, "x-codex-active-limit")?.trim();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const XAI_PROVIDER = "xai_subscription";
|
|
2
|
+
export const XAI_DEFAULT_SCOPES = [
|
|
3
|
+
"openid",
|
|
4
|
+
"profile",
|
|
5
|
+
"email",
|
|
6
|
+
"offline_access",
|
|
7
|
+
"grok-cli:access",
|
|
8
|
+
"api:access",
|
|
9
|
+
];
|
|
10
|
+
function parseExpiresAt(value) {
|
|
11
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
12
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
13
|
+
throw new Error("expiresAt must be a positive Unix timestamp in milliseconds");
|
|
14
|
+
}
|
|
15
|
+
return parsed;
|
|
16
|
+
}
|
|
17
|
+
function parseScopes(value) {
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return value.filter(Boolean);
|
|
20
|
+
if (typeof value === "string")
|
|
21
|
+
return value.split(/\s+/).filter(Boolean);
|
|
22
|
+
return [...XAI_DEFAULT_SCOPES];
|
|
23
|
+
}
|
|
24
|
+
export function createXaiAccountRecord(input) {
|
|
25
|
+
const id = input.id.trim();
|
|
26
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
27
|
+
throw new Error("Only letters, numbers, _ and - allowed in account ID");
|
|
28
|
+
}
|
|
29
|
+
if (!input.accessToken.trim())
|
|
30
|
+
throw new Error("Access token is required");
|
|
31
|
+
if (!input.refreshToken.trim())
|
|
32
|
+
throw new Error("Refresh token is required");
|
|
33
|
+
return {
|
|
34
|
+
id,
|
|
35
|
+
provider: XAI_PROVIDER,
|
|
36
|
+
accessToken: input.accessToken.trim(),
|
|
37
|
+
refreshToken: input.refreshToken.trim(),
|
|
38
|
+
expiresAt: parseExpiresAt(input.expiresAt),
|
|
39
|
+
scopes: parseScopes(input.scopes),
|
|
40
|
+
enabled: input.enabled ?? true,
|
|
41
|
+
};
|
|
42
|
+
}
|