@pikoloo/codex-proxy 1.0.6

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +199 -0
  3. package/bin/cli.js +118 -0
  4. package/docs/ACCOUNTS.md +202 -0
  5. package/docs/API.md +289 -0
  6. package/docs/ARCHITECTURE.md +129 -0
  7. package/docs/CLAUDE_INTEGRATION.md +163 -0
  8. package/docs/OAUTH.md +85 -0
  9. package/docs/OPENCLAW.md +34 -0
  10. package/docs/legal.md +11 -0
  11. package/images/dashboard-screenshot.png +0 -0
  12. package/images/demo-screenshot.png +0 -0
  13. package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
  14. package/package.json +61 -0
  15. package/public/css/style.css +1502 -0
  16. package/public/index.html +827 -0
  17. package/public/js/app.js +601 -0
  18. package/src/account-manager.js +528 -0
  19. package/src/account-rotation/index.js +93 -0
  20. package/src/account-rotation/rate-limits.js +293 -0
  21. package/src/account-rotation/strategies/base-strategy.js +48 -0
  22. package/src/account-rotation/strategies/index.js +31 -0
  23. package/src/account-rotation/strategies/round-robin-strategy.js +42 -0
  24. package/src/account-rotation/strategies/sticky-strategy.js +97 -0
  25. package/src/claude-config.js +153 -0
  26. package/src/cli/accounts.js +557 -0
  27. package/src/direct-api.js +164 -0
  28. package/src/format-converter.js +420 -0
  29. package/src/index.js +46 -0
  30. package/src/kilo-api.js +68 -0
  31. package/src/kilo-format-converter.js +285 -0
  32. package/src/kilo-models.js +103 -0
  33. package/src/kilo-streamer.js +243 -0
  34. package/src/middleware/credentials.js +116 -0
  35. package/src/middleware/sse.js +96 -0
  36. package/src/model-api.js +189 -0
  37. package/src/model-mapper.js +157 -0
  38. package/src/oauth.js +666 -0
  39. package/src/response-streamer.js +409 -0
  40. package/src/routes/accounts-route.js +332 -0
  41. package/src/routes/api-routes.js +98 -0
  42. package/src/routes/chat-route.js +229 -0
  43. package/src/routes/claude-config-route.js +121 -0
  44. package/src/routes/logs-route.js +43 -0
  45. package/src/routes/messages-route.js +203 -0
  46. package/src/routes/models-route.js +119 -0
  47. package/src/routes/settings-route.js +143 -0
  48. package/src/security.js +142 -0
  49. package/src/server-settings.js +56 -0
  50. package/src/server.js +58 -0
  51. package/src/signature-cache.js +106 -0
  52. package/src/thinking-utils.js +312 -0
  53. package/src/utils/logger.js +156 -0
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Credentials Middleware
3
+ * Resolves and validates the active account credentials,
4
+ * auto-refreshing tokens when they are expired or expiring soon.
5
+ */
6
+
7
+ import {
8
+ getActiveAccount,
9
+ refreshAccountToken,
10
+ isTokenExpiredOrExpiringSoon,
11
+ loadAccounts
12
+ } from '../account-manager.js';
13
+ import { logger } from '../utils/logger.js';
14
+
15
+ /**
16
+ * Resolves the active account credentials, refreshing the token if needed.
17
+ * Returns null if no valid account is available.
18
+ *
19
+ * @returns {Promise<{accessToken: string, accountId: string, email: string}|null>}
20
+ */
21
+ export async function getCredentialsOrError() {
22
+ const account = getActiveAccount();
23
+
24
+ if (!account) {
25
+ logger.info('No active account found');
26
+ return null;
27
+ }
28
+
29
+ if (!account.accessToken || !account.accountId) {
30
+ logger.info(`Account ${account.email} missing token or accountId`);
31
+ return null;
32
+ }
33
+
34
+ if (isTokenExpiredOrExpiringSoon(account)) {
35
+ logger.info(`Token expired/expiring soon for ${account.email}, refreshing...`);
36
+ const result = await refreshAccountToken(account.email);
37
+
38
+ if (!result.success) {
39
+ logger.error(`Failed to refresh token: ${result.message}`);
40
+ return null;
41
+ }
42
+
43
+ const refreshedAccount = getActiveAccount();
44
+ if (!refreshedAccount) {
45
+ logger.error('Failed to get refreshed account');
46
+ return null;
47
+ }
48
+
49
+ logger.info(`Using refreshed token for ${refreshedAccount.email}`);
50
+ return {
51
+ accessToken: refreshedAccount.accessToken,
52
+ accountId: refreshedAccount.accountId,
53
+ email: refreshedAccount.email
54
+ };
55
+ }
56
+
57
+ return {
58
+ accessToken: account.accessToken,
59
+ accountId: account.accountId,
60
+ email: account.email
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Get credentials for a specific account by email.
66
+ * @param {string} email
67
+ * @returns {Promise<{accessToken: string, accountId: string, email: string}|null>}
68
+ */
69
+ export async function getCredentialsForAccount(email) {
70
+ const data = loadAccounts();
71
+ const account = data.accounts.find(a => a.email === email);
72
+
73
+ if (!account) {
74
+ return null;
75
+ }
76
+
77
+ if (!account.accessToken || !account.accountId) {
78
+ return null;
79
+ }
80
+
81
+ if (isTokenExpiredOrExpiringSoon(account)) {
82
+ const result = await refreshAccountToken(account.email);
83
+ if (!result.success) {
84
+ return null;
85
+ }
86
+ const refreshedData = loadAccounts();
87
+ const refreshedAccount = refreshedData.accounts.find(a => a.email === email);
88
+ if (!refreshedAccount) return null;
89
+
90
+ return {
91
+ accessToken: refreshedAccount.accessToken,
92
+ accountId: refreshedAccount.accountId,
93
+ email: refreshedAccount.email
94
+ };
95
+ }
96
+
97
+ return {
98
+ accessToken: account.accessToken,
99
+ accountId: account.accountId,
100
+ email: account.email
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Sends a 401 authentication error response.
106
+ * @param {import('express').Response} res
107
+ * @param {string} [message]
108
+ */
109
+ export function sendAuthError(res, message = 'No active account with valid credentials. Add an account via /accounts/add') {
110
+ return res.status(401).json({
111
+ type: 'error',
112
+ error: { type: 'authentication_error', message }
113
+ });
114
+ }
115
+
116
+ export default { getCredentialsOrError, getCredentialsForAccount, sendAuthError };
@@ -0,0 +1,96 @@
1
+ /**
2
+ * SSE Helpers
3
+ * Shared utilities for Server-Sent Events streaming and error responses.
4
+ */
5
+
6
+ import { formatSSEEvent } from '../response-streamer.js';
7
+ import { logger } from '../utils/logger.js';
8
+
9
+ /**
10
+ * Sets the standard SSE response headers and flushes them.
11
+ * @param {import('express').Response} res
12
+ */
13
+ export function initSSEResponse(res) {
14
+ res.setHeader('Content-Type', 'text/event-stream');
15
+ res.setHeader('Cache-Control', 'no-cache');
16
+ res.setHeader('Connection', 'keep-alive');
17
+ res.setHeader('X-Accel-Buffering', 'no');
18
+ res.flushHeaders();
19
+ }
20
+
21
+ /**
22
+ * Streams an async generator of Anthropic-format SSE events to the response.
23
+ * Writes [DONE] and ends the response when the generator is exhausted.
24
+ *
25
+ * @param {import('express').Response} res
26
+ * @param {AsyncIterable<object>} eventStream
27
+ */
28
+ export async function pipeSSEStream(res, eventStream) {
29
+ for await (const event of eventStream) {
30
+ res.write(formatSSEEvent(event));
31
+ }
32
+ res.write('data: [DONE]\n\n');
33
+ res.end();
34
+ }
35
+
36
+ /**
37
+ * Sends a structured Anthropic-style error JSON response.
38
+ * If headers have already been sent (mid-stream), writes an SSE error event instead.
39
+ *
40
+ * @param {import('express').Response} res
41
+ * @param {Error} error
42
+ * @param {string} model
43
+ * @param {number} startTime
44
+ */
45
+ export function handleStreamError(res, error, model, startTime) {
46
+ const duration = Date.now() - startTime;
47
+ logger.response(500, { model, error: error.message, duration });
48
+
49
+ if (res.headersSent) {
50
+ res.write(
51
+ `event: error\ndata: ${JSON.stringify({
52
+ type: 'error',
53
+ error: { type: 'api_error', message: error.message }
54
+ })}\n\n`
55
+ );
56
+ res.end();
57
+ return;
58
+ }
59
+
60
+ if (error.message.includes('AUTH_EXPIRED')) {
61
+ return res.status(401).json({
62
+ type: 'error',
63
+ error: { type: 'authentication_error', message: 'Token expired. Please refresh or re-authenticate.' }
64
+ });
65
+ }
66
+
67
+ if (error.message.startsWith('RATE_LIMITED:')) {
68
+ const parts = error.message.split(':');
69
+ const resetMs = parseInt(parts[1], 10);
70
+ const errorText = parts.slice(2).join(':') || error.message;
71
+
72
+ return res.status(429).json({
73
+ type: 'error',
74
+ error: {
75
+ type: 'rate_limit_error',
76
+ message: errorText,
77
+ resetMs: resetMs,
78
+ resetSeconds: Math.round(resetMs / 1000)
79
+ }
80
+ });
81
+ }
82
+
83
+ if (error.message.includes('RESOURCE_EXHAUSTED')) {
84
+ return res.status(429).json({
85
+ type: 'error',
86
+ error: { type: 'rate_limit_error', message: error.message }
87
+ });
88
+ }
89
+
90
+ res.status(500).json({
91
+ type: 'error',
92
+ error: { type: 'api_error', message: error.message }
93
+ });
94
+ }
95
+
96
+ export default { initSSEResponse, pipeSSEStream, handleStreamError };
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Model API for ChatGPT Codex
3
+ * Handles model listing and quota retrieval from ChatGPT backend API.
4
+ */
5
+
6
+ const CHATGPT_API_BASE = 'https://chatgpt.com/backend-api';
7
+ const CLIENT_VERSION = '0.100.0';
8
+
9
+ const MODEL_CACHE = {
10
+ models: null,
11
+ lastFetched: 0,
12
+ ttlMs: 5 * 60 * 1000
13
+ };
14
+
15
+ export async function fetchModels(accessToken, accountId) {
16
+ const now = Date.now();
17
+
18
+ if (MODEL_CACHE.models && (now - MODEL_CACHE.lastFetched) < MODEL_CACHE.ttlMs) {
19
+ return MODEL_CACHE.models;
20
+ }
21
+
22
+ const url = `${CHATGPT_API_BASE}/codex/models?client_version=${CLIENT_VERSION}`;
23
+
24
+ const response = await fetch(url, {
25
+ method: 'GET',
26
+ headers: {
27
+ 'Authorization': `Bearer ${accessToken}`,
28
+ 'ChatGPT-Account-ID': accountId,
29
+ 'Accept': 'application/json'
30
+ }
31
+ });
32
+
33
+ if (!response.ok) {
34
+ const errorText = await response.text();
35
+ throw new Error(`Failed to fetch models: ${response.status} - ${errorText}`);
36
+ }
37
+
38
+ const data = await response.json();
39
+
40
+ const models = (data.models || []).map(m => ({
41
+ id: m.slug,
42
+ name: m.display_name || m.slug,
43
+ description: m.description || '',
44
+ defaultReasoningLevel: m.default_reasoning_level || 'medium',
45
+ supportedReasoningLevels: m.supported_reasoning_levels || [],
46
+ supportedInApi: m.supported_in_api || false,
47
+ visibility: m.visibility || 'list'
48
+ }));
49
+
50
+ MODEL_CACHE.models = models;
51
+ MODEL_CACHE.lastFetched = now;
52
+
53
+ return models;
54
+ }
55
+
56
+ export async function fetchUsage(accessToken, accountId) {
57
+ const url = `${CHATGPT_API_BASE}/wham/usage`;
58
+
59
+ const response = await fetch(url, {
60
+ method: 'GET',
61
+ headers: {
62
+ 'Authorization': `Bearer ${accessToken}`,
63
+ 'ChatGPT-Account-ID': accountId,
64
+ 'Accept': 'application/json'
65
+ }
66
+ });
67
+
68
+ if (!response.ok) {
69
+ const errorText = await response.text();
70
+ throw new Error(`Failed to fetch usage: ${response.status} - ${errorText}`);
71
+ }
72
+
73
+ const data = await response.json();
74
+
75
+ const primaryWindow = data.rate_limit?.primary_window || {};
76
+ const usedPercentRaw = Number(primaryWindow?.used_percent);
77
+ const usedPercent = Number.isFinite(usedPercentRaw) ? usedPercentRaw : 0;
78
+
79
+ const limitWindowSecondsRaw = Number(primaryWindow?.limit_window_seconds);
80
+ const limitWindowSeconds = Number.isFinite(limitWindowSecondsRaw) ? limitWindowSecondsRaw : null;
81
+
82
+ const resetAfterSecondsRaw = Number(primaryWindow?.reset_after_seconds);
83
+ const resetAfterSeconds = Number.isFinite(resetAfterSecondsRaw) ? resetAfterSecondsRaw : null;
84
+
85
+ const resetAtEpoch = Number(primaryWindow?.reset_at);
86
+ const resetAt = Number.isFinite(resetAtEpoch) ? new Date(resetAtEpoch * 1000).toISOString() : null;
87
+
88
+ return {
89
+ totalTokenUsage: usedPercent,
90
+ limit: 100,
91
+ remaining: 100 - usedPercent,
92
+ percentage: usedPercent,
93
+ resetAt: resetAt,
94
+ resetAfterSeconds: resetAfterSeconds,
95
+ limitWindowSeconds: limitWindowSeconds,
96
+ planType: data.plan_type || null,
97
+ limitReached: data.rate_limit?.limit_reached || false,
98
+ allowed: data.rate_limit?.allowed ?? true,
99
+ raw: data
100
+ };
101
+ }
102
+
103
+ export async function fetchAccountCheck(accessToken, accountId) {
104
+ const url = `${CHATGPT_API_BASE}/wham/accounts/check`;
105
+
106
+ const response = await fetch(url, {
107
+ method: 'GET',
108
+ headers: {
109
+ 'Authorization': `Bearer ${accessToken}`,
110
+ 'ChatGPT-Account-ID': accountId,
111
+ 'Accept': 'application/json'
112
+ }
113
+ });
114
+
115
+ if (!response.ok) {
116
+ const errorText = await response.text();
117
+ throw new Error(`Failed to fetch account check: ${response.status} - ${errorText}`);
118
+ }
119
+
120
+ return await response.json();
121
+ }
122
+
123
+ export async function getAccountQuota(accessToken, accountId) {
124
+ try {
125
+ const [usage, accountCheck] = await Promise.allSettled([
126
+ fetchUsage(accessToken, accountId),
127
+ fetchAccountCheck(accessToken, accountId)
128
+ ]);
129
+
130
+ const quotaInfo = {
131
+ usage: usage.status === 'fulfilled' ? usage.value : null,
132
+ account: accountCheck.status === 'fulfilled' ? accountCheck.value : null,
133
+ fetchedAt: new Date().toISOString()
134
+ };
135
+
136
+ if (usage.status === 'rejected') {
137
+ quotaInfo.usageError = usage.reason?.message || 'Unknown error';
138
+ }
139
+
140
+ if (accountCheck.status === 'rejected') {
141
+ quotaInfo.accountError = accountCheck.reason?.message || 'Unknown error';
142
+ }
143
+
144
+ return quotaInfo;
145
+ } catch (error) {
146
+ return {
147
+ usage: null,
148
+ account: null,
149
+ error: error.message,
150
+ fetchedAt: new Date().toISOString()
151
+ };
152
+ }
153
+ }
154
+
155
+ export async function getModelsAndQuota(accessToken, accountId) {
156
+ try {
157
+ const [models, quota] = await Promise.all([
158
+ fetchModels(accessToken, accountId),
159
+ getAccountQuota(accessToken, accountId)
160
+ ]);
161
+
162
+ return {
163
+ models,
164
+ quota,
165
+ success: true
166
+ };
167
+ } catch (error) {
168
+ return {
169
+ models: null,
170
+ quota: null,
171
+ error: error.message,
172
+ success: false
173
+ };
174
+ }
175
+ }
176
+
177
+ export function clearModelCache() {
178
+ MODEL_CACHE.models = null;
179
+ MODEL_CACHE.lastFetched = 0;
180
+ }
181
+
182
+ export default {
183
+ fetchModels,
184
+ fetchUsage,
185
+ fetchAccountCheck,
186
+ getAccountQuota,
187
+ getModelsAndQuota,
188
+ clearModelCache
189
+ };
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Model Mapper
3
+ * Maps Anthropic/Claude model names to upstream OpenAI/Kilo model identifiers.
4
+ */
5
+
6
+ import { getServerSettings } from './server-settings.js';
7
+
8
+ const DEFAULT_OPENAI_MODEL = 'gpt-5.5';
9
+ const DEFAULT_SMALL_OPENAI_MODEL = 'gpt-5.4-mini';
10
+ const LATEST_CODEX_MODEL = 'gpt-5.3-codex';
11
+ const KILO_ENABLED_ENV = 'CODEX_CLAUDE_PROXY_ENABLE_KILO';
12
+
13
+ const CLAUDE_MODEL_MAP = {
14
+ // Current Claude 4.6 models (Feb 2026)
15
+ 'claude-opus-4-6': DEFAULT_OPENAI_MODEL,
16
+ 'claude-opus-4-6-20250219': DEFAULT_OPENAI_MODEL,
17
+ 'claude-sonnet-4-6': DEFAULT_OPENAI_MODEL,
18
+ 'claude-sonnet-4-6-20250219': DEFAULT_OPENAI_MODEL,
19
+ 'claude-haiku-4-5': DEFAULT_SMALL_OPENAI_MODEL,
20
+ 'claude-haiku-4-5-20250219': DEFAULT_SMALL_OPENAI_MODEL,
21
+
22
+ // 1M context variants
23
+ 'claude-opus-4-6-1m': DEFAULT_OPENAI_MODEL,
24
+ 'claude-sonnet-4-6-1m': DEFAULT_OPENAI_MODEL,
25
+
26
+ // Legacy Claude 4.5 models (deprecated but still supported)
27
+ 'claude-opus-4-5': DEFAULT_OPENAI_MODEL,
28
+ 'claude-opus-4-5-20250514': DEFAULT_OPENAI_MODEL,
29
+ 'claude-sonnet-4-5': DEFAULT_OPENAI_MODEL,
30
+ 'claude-sonnet-4-5-20250514': DEFAULT_OPENAI_MODEL,
31
+ 'claude-sonnet-4-20250514': DEFAULT_OPENAI_MODEL,
32
+ 'claude-haiku-4-20250514': DEFAULT_SMALL_OPENAI_MODEL,
33
+ 'claude-haiku-3-5-20250514': DEFAULT_SMALL_OPENAI_MODEL,
34
+
35
+ // Legacy Claude 3.x models
36
+ 'claude-3-5-sonnet-20240620': DEFAULT_OPENAI_MODEL,
37
+ 'claude-3-opus-20240229': DEFAULT_OPENAI_MODEL,
38
+ 'claude-3-sonnet-20240229': DEFAULT_OPENAI_MODEL,
39
+ 'claude-3-haiku-20240307': DEFAULT_SMALL_OPENAI_MODEL,
40
+
41
+ // Short aliases
42
+ 'sonnet': DEFAULT_OPENAI_MODEL,
43
+ 'opus': DEFAULT_OPENAI_MODEL,
44
+ 'haiku': DEFAULT_SMALL_OPENAI_MODEL,
45
+ 'codex': LATEST_CODEX_MODEL,
46
+ 'kilo': 'kilo',
47
+
48
+ // Direct OpenAI models
49
+ 'gpt-5.5': 'gpt-5.5',
50
+ 'gpt-5.5-2026-04-23': 'gpt-5.5-2026-04-23',
51
+ 'gpt-5.4': 'gpt-5.4',
52
+ 'gpt-5.4-2026-03-05': 'gpt-5.4-2026-03-05',
53
+ 'gpt-5.4-mini': 'gpt-5.4-mini',
54
+ 'gpt-5.4-nano': 'gpt-5.4-nano',
55
+ 'gpt-5.3-codex': 'gpt-5.3-codex',
56
+ 'gpt-5.2-codex': 'gpt-5.2-codex',
57
+ 'gpt-5.1-codex-max': 'gpt-5.1-codex-max',
58
+ 'gpt-5.1-codex': 'gpt-5.1-codex',
59
+ 'gpt-5-codex': 'gpt-5-codex',
60
+ 'gpt-5.2': 'gpt-5.2',
61
+ 'gpt-5.1': 'gpt-5.1',
62
+ 'gpt-5': 'gpt-5',
63
+ 'gpt-5.1-codex-mini': 'gpt-5.1-codex-mini',
64
+ 'gpt-5-codex-mini': 'gpt-5-codex-mini'
65
+ };
66
+
67
+ /**
68
+ * Maps a Claude/Anthropic model name to the upstream model identifier.
69
+ * Falls back to the current OpenAI flagship model for unknown models.
70
+ * @param {string} model
71
+ * @returns {string}
72
+ */
73
+ export function mapClaudeModel(model) {
74
+ if (!model) return DEFAULT_OPENAI_MODEL;
75
+
76
+ if (CLAUDE_MODEL_MAP[model]) {
77
+ return CLAUDE_MODEL_MAP[model];
78
+ }
79
+
80
+ const modelLower = model.toLowerCase();
81
+
82
+ if (modelLower.startsWith('gpt-')) {
83
+ return modelLower;
84
+ }
85
+
86
+ if (modelLower.startsWith('claude-')) {
87
+ const cleanModel = modelLower.replace(/^claude-/, '');
88
+ if (cleanModel.includes('opus')) return DEFAULT_OPENAI_MODEL;
89
+ if (cleanModel.includes('sonnet')) return DEFAULT_OPENAI_MODEL;
90
+ if (cleanModel.includes('haiku')) return DEFAULT_SMALL_OPENAI_MODEL;
91
+ }
92
+
93
+ for (const [key, value] of Object.entries(CLAUDE_MODEL_MAP)) {
94
+ if (modelLower.includes(key.toLowerCase())) {
95
+ return value;
96
+ }
97
+ }
98
+
99
+ return DEFAULT_OPENAI_MODEL;
100
+ }
101
+
102
+ /**
103
+ * Returns true if the mapped model should be routed through Kilo.
104
+ * @param {string} mappedModel
105
+ * @returns {boolean}
106
+ */
107
+ export function isKiloModel(mappedModel) {
108
+ return mappedModel === 'kilo';
109
+ }
110
+
111
+ export function isKiloEnabled() {
112
+ return process.env[KILO_ENABLED_ENV] === 'true';
113
+ }
114
+
115
+ /**
116
+ * Resolves the actual Kilo model identifier based on server settings.
117
+ * The setting stores the full Kilo model ID (e.g. 'minimax/minimax-m2.5:free').
118
+ * @returns {string}
119
+ */
120
+ export function resolveKiloModel() {
121
+ const settings = getServerSettings();
122
+ return settings.haikuKiloModel || 'minimax/minimax-m2.5:free';
123
+ }
124
+
125
+ /**
126
+ * Resolves all model routing info from a requested model name.
127
+ * @param {string} requestedModel
128
+ * @returns {{ mappedModel: string, isKilo: boolean, kiloTarget: string|null, upstreamModel: string }}
129
+ */
130
+ export function resolveModelRouting(requestedModel) {
131
+ const mappedModel = mapClaudeModel(requestedModel || DEFAULT_OPENAI_MODEL);
132
+ const isKilo = isKiloModel(mappedModel);
133
+ const kiloTarget = isKilo ? resolveKiloModel() : null;
134
+ const upstreamModel = isKilo ? kiloTarget : mappedModel;
135
+ return { mappedModel, isKilo, kiloTarget, upstreamModel };
136
+ }
137
+
138
+ export {
139
+ CLAUDE_MODEL_MAP,
140
+ DEFAULT_OPENAI_MODEL,
141
+ DEFAULT_SMALL_OPENAI_MODEL,
142
+ LATEST_CODEX_MODEL,
143
+ KILO_ENABLED_ENV
144
+ };
145
+
146
+ export default {
147
+ mapClaudeModel,
148
+ isKiloModel,
149
+ resolveKiloModel,
150
+ resolveModelRouting,
151
+ CLAUDE_MODEL_MAP,
152
+ DEFAULT_OPENAI_MODEL,
153
+ DEFAULT_SMALL_OPENAI_MODEL,
154
+ LATEST_CODEX_MODEL,
155
+ KILO_ENABLED_ENV,
156
+ isKiloEnabled
157
+ };