@exreve/exk 1.0.77 → 1.0.79

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.
@@ -56,7 +56,27 @@ const symlinkAsync = promisify(fsSymlink);
56
56
  // AI config - loaded from server after registration, stored in ~/.talk-to-code/ai-config.json
57
57
  // (Do not read ANTHROPIC_* / CLAUDE_MODEL from the host environment — only this file + code default model.)
58
58
  const AI_CONFIG_PATH = path.join(os.homedir(), '.talk-to-code', 'ai-config.json');
59
- const DEFAULT_AI_MODEL = 'glm-5.2';
59
+ const DEFAULT_AI_MODEL = 'GLM-5.2';
60
+ /**
61
+ * Normalize a model ID before sending it to the upstream API.
62
+ *
63
+ * z.ai's API only accepts UPPERCASE model IDs (GLM-5.2, GLM-5.1, etc.). The
64
+ * lowercase form 'glm-5.2' triggers a 529 overloaded_error from z.ai, even
65
+ * though it's a real model on their side. The session schema in MongoDB and
66
+ * existing ai-config.json files may still contain the legacy lowercase form,
67
+ * so we normalize defensively at every read point.
68
+ */
69
+ function normalizeModelId(model) {
70
+ if (!model)
71
+ return DEFAULT_AI_MODEL;
72
+ const m = String(model).trim();
73
+ if (!m)
74
+ return DEFAULT_AI_MODEL;
75
+ // z.ai GLM models must be uppercase
76
+ if (/^glm[-.]/i.test(m))
77
+ return m.toUpperCase();
78
+ return m;
79
+ }
60
80
  /** TTL cache for ai-config.json reads to avoid hitting disk on every call */
61
81
  let _aiConfigCache = null;
62
82
  const AI_CONFIG_TTL_MS = 5_000;
@@ -66,7 +86,7 @@ const PROVIDERS = {
66
86
  zai: {
67
87
  apiKey: process.env.ZHIPU_API_KEY || '',
68
88
  baseUrl: process.env.CLI_AI_BASE_URL || 'https://api.z.ai/api/anthropic',
69
- models: ['glm-5.2'],
89
+ models: ['GLM-5.2'],
70
90
  },
71
91
  minimax: {
72
92
  apiKey: '',
@@ -97,10 +117,11 @@ function rebuildProvidersFromConfig(models) {
97
117
  const providerModels = {};
98
118
  const providerUrls = {};
99
119
  for (const m of models) {
120
+ const id = normalizeModelId(m.id);
100
121
  if (!providerModels[m.provider])
101
122
  providerModels[m.provider] = [];
102
- if (!providerModels[m.provider].includes(m.id))
103
- providerModels[m.provider].push(m.id);
123
+ if (!providerModels[m.provider].includes(id))
124
+ providerModels[m.provider].push(id);
104
125
  if (m.providerBaseUrl)
105
126
  providerUrls[m.provider] = m.providerBaseUrl;
106
127
  }
@@ -129,15 +150,17 @@ function resolveProvider(model, providerId) {
129
150
  PROVIDERS.kimi.apiKey = aiConfig.kimiApiKey || process.env.KIMI_API_KEY || '';
130
151
  if (!PROVIDERS.zai.apiKey)
131
152
  PROVIDERS.zai.apiKey = aiConfig.apiKey || '';
153
+ // Normalize model name for z.ai case-sensitivity
154
+ const normalizedModel = normalizeModelId(model);
132
155
  // 1. Explicit provider selection
133
156
  if (providerId && PROVIDERS[providerId]?.apiKey) {
134
157
  const provider = PROVIDERS[providerId];
135
- return { provider: providerId, apiKey: provider.apiKey, baseUrl: provider.baseUrl, model };
158
+ return { provider: providerId, apiKey: provider.apiKey, baseUrl: provider.baseUrl, model: normalizedModel };
136
159
  }
137
160
  // 2. Match model name to a provider
138
161
  for (const [id, config] of Object.entries(PROVIDERS)) {
139
- if (config.models.includes(model) && config.apiKey) {
140
- return { provider: id, apiKey: config.apiKey, baseUrl: config.baseUrl, model };
162
+ if (config.models.includes(normalizedModel) && config.apiKey) {
163
+ return { provider: id, apiKey: config.apiKey, baseUrl: config.baseUrl, model: normalizedModel };
141
164
  }
142
165
  }
143
166
  // 3. Fallback: use ai-config.json credentials (z.ai default)
@@ -145,7 +168,7 @@ function resolveProvider(model, providerId) {
145
168
  provider: 'zai',
146
169
  apiKey: aiConfig.apiKey,
147
170
  baseUrl: aiConfig.baseUrl || PROVIDERS.zai.baseUrl,
148
- model,
171
+ model: normalizedModel,
149
172
  };
150
173
  }
151
174
  function loadAiConfig() {
@@ -158,7 +181,7 @@ function loadAiConfig() {
158
181
  const config = JSON.parse(data);
159
182
  const apiKey = typeof config.authToken === 'string' ? config.authToken.trim() : '';
160
183
  const baseUrl = typeof config.baseUrl === 'string' ? config.baseUrl.trim() : '';
161
- const model = typeof config.model === 'string' && config.model.trim() ? config.model.trim() : DEFAULT_AI_MODEL;
184
+ const model = normalizeModelId(typeof config.model === 'string' && config.model.trim() ? config.model.trim() : DEFAULT_AI_MODEL);
162
185
  const proxy = typeof config.proxy === 'string' ? config.proxy.trim() : '';
163
186
  const minimaxApiKey = typeof config.minimaxApiKey === 'string' ? config.minimaxApiKey.trim() : '';
164
187
  const openrouterApiKey = typeof config.openrouterApiKey === 'string' ? config.openrouterApiKey.trim() : '';
@@ -239,6 +262,17 @@ function envForClaudeCodeChild(_localModel, resolvedProvider) {
239
262
  env.ANTHROPIC_API_KEY = effectiveApiKey;
240
263
  if (effectiveBaseUrl)
241
264
  env.ANTHROPIC_BASE_URL = effectiveBaseUrl;
265
+ // For z.ai specifically: override ALL model aliases so the SDK
266
+ // sends the correct (UPPERCASE) model ID. z.ai's API is case-sensitive
267
+ // and rejects lowercase 'glm-5.2' with 529 overloaded_error.
268
+ if (resolvedProvider?.provider === 'zai') {
269
+ const m = normalizeModelId(resolvedProvider.model);
270
+ env.ANTHROPIC_MODEL = m;
271
+ env.ANTHROPIC_DEFAULT_SONNET_MODEL = m;
272
+ env.ANTHROPIC_DEFAULT_OPUS_MODEL = m;
273
+ env.ANTHROPIC_DEFAULT_HAIKU_MODEL = m;
274
+ env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1';
275
+ }
242
276
  // For MiniMax specifically: override ALL model aliases so the SDK
243
277
  // sends the correct model ID to the Anthropic-compatible endpoint
244
278
  if (resolvedProvider?.provider === 'minimax') {
@@ -686,6 +720,15 @@ export class AgentSessionManager {
686
720
  ANTHROPIC_API_KEY: resolved.apiKey,
687
721
  ANTHROPIC_BASE_URL: resolved.baseUrl,
688
722
  };
723
+ // For z.ai: also override all model aliases in settings (case-sensitive)
724
+ if (resolved.provider === 'zai') {
725
+ const m = normalizeModelId(resolved.model);
726
+ settingsEnv.ANTHROPIC_MODEL = m;
727
+ settingsEnv.ANTHROPIC_DEFAULT_SONNET_MODEL = m;
728
+ settingsEnv.ANTHROPIC_DEFAULT_OPUS_MODEL = m;
729
+ settingsEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL = m;
730
+ settingsEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = '1';
731
+ }
689
732
  // For MiniMax: also override all model aliases in settings
690
733
  if (resolved.provider === 'minimax') {
691
734
  settingsEnv.ANTHROPIC_MODEL = resolved.model;
@@ -169,6 +169,34 @@ export class ClaudeBackend {
169
169
  return CACHED_CLAUDE_PATH;
170
170
  }
171
171
  async *executePrompt(prompt, config) {
172
+ const { cwd, apiKey, model, env, settings, signal, attachmentDir, routingSessionId, routingPromptId, resumeSessionId } = config;
173
+ // Retry config for 529 overloaded errors
174
+ const MAX_RETRIES = 2;
175
+ const RETRY_DELAY_MS = 3000;
176
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
177
+ if (attempt > 0) {
178
+ console.log(`[ClaudeBackend] Retry ${attempt}/${MAX_RETRIES} after 529 delay...`);
179
+ await new Promise(r => setTimeout(r, RETRY_DELAY_MS * attempt));
180
+ }
181
+ try {
182
+ const stream = this._executePromptOnce(prompt, {
183
+ cwd, apiKey, model, env, settings, signal, attachmentDir, routingSessionId, routingPromptId, resumeSessionId,
184
+ });
185
+ for await (const event of stream) {
186
+ yield event;
187
+ }
188
+ return; // Success — exit retry loop
189
+ }
190
+ catch (err) {
191
+ const is529 = err?.message?.includes('529') || err?.message?.includes('overloaded_error');
192
+ if (!is529 || attempt === MAX_RETRIES) {
193
+ throw err; // Not retryable or out of retries
194
+ }
195
+ console.log(`[ClaudeBackend] 529 overloaded detected, will retry: ${err.message}`);
196
+ }
197
+ }
198
+ }
199
+ async *_executePromptOnce(prompt, config) {
172
200
  const { cwd, apiKey, model, env, settings, signal, attachmentDir, routingSessionId, routingPromptId, resumeSessionId } = config;
173
201
  // Build MCP server for this query
174
202
  const mcpServer = createModuleMcpServer({
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exreve/exk",
3
- "version": "1.0.77",
3
+ "version": "1.0.79",
4
4
  "description": "exk - Control Claude CLI with voice and programmable interfaces",
5
5
  "type": "module",
6
6
  "bin": {