@lingjingai/scriptctl 0.63.3 → 0.63.4

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/README.md CHANGED
@@ -136,7 +136,7 @@ MIT
136
136
 
137
137
  ### 指定视频理解服务
138
138
 
139
- Gemini 文本、结构化工具和视频请求在当前路由重试耗尽或被审核拦截后,由已配置的豆包兜底。视频的默认路由与显式指定的 Gemini 均保留所选主模型;视频上传失败时豆包读取同一份本地文件。用户取消不触发兜底。豆包使用 `ARK_API_KEY`,`ARK_MODEL` 可指定已开通的视频理解端点,默认 `ep-20260721150136-98dj9`。直接选择豆包时只调用豆包:
139
+ Gemini 文本、结构化工具和视频请求先使用配置的主模型,再按 `gemini-3.8-flash → gemini-3.7-flash → gemini-3.6-flash` 尝试尚未调用的模型,全部失败后使用豆包。每个 Gemini 模型按 provider 的 `maxAttempts` 重试暂时性错误,豆包最多尝试 3 次;审核拒绝等不可重试错误直接进入下一模型,用户取消立即停止。切换 Gemini 模型保留服务地址、凭据和同一份视频;公共 Files API 上传重试耗尽时,由豆包读取本地视频。豆包使用 `ARK_API_KEY`,`ARK_MODEL` 可指定已开通的视频理解端点,默认 `ep-20260721150136-98dj9`。完整模型链耗尽后才汇总报错,调用和用量逐模型记录。直接选择豆包时只调用豆包:
140
140
 
141
141
  ```bash
142
142
  # 豆包:默认读取 ARK_API_KEY;模型也可通过 ARK_MODEL 设置
@@ -0,0 +1,5 @@
1
+ # 0.63.4
2
+
3
+ - Gemini 文本、结构化工具和视频请求先使用所选模型,再按 3.8 Flash → 3.7 Flash → 3.6 Flash 补齐尚未尝试的模型,最后由豆包兜底。
4
+ - 每个 Gemini 模型独立执行配置的暂时性错误重试,豆包保留最多 3 次尝试;完整模型链耗尽后汇总报错,取消立即停止。
5
+ - 复用 Gemini 视频媒体与已有分集检查点,保留各模型的实际用量、重试次数和失败原因。
package/dist/bin.js CHANGED
@@ -7802,6 +7802,7 @@ var DOUBAO_FALLBACK = {
7802
7802
  timeoutMs: 6e5,
7803
7803
  maxAttempts: 3
7804
7804
  };
7805
+ var GEMINI_FLASH_MODELS = ["gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.6-flash"];
7805
7806
  function normalizeBaseUrl(value) {
7806
7807
  return value?.trim().replace(/\/+$/, "") ?? "anthropic-sdk-default";
7807
7808
  }
@@ -7842,9 +7843,9 @@ function routedFailure(error, route) {
7842
7843
  message: error.message
7843
7844
  });
7844
7845
  }
7845
- function fallbackFailure(primary, error, route) {
7846
- const failures = [primary, routedFailure(error, route)];
7847
- const aggregate = aggregateFailures(failures, "Primary model and Doubao fallback both failed.");
7846
+ function exhaustedFailure(error, failures) {
7847
+ if (failures.length === 1) return error;
7848
+ const aggregate = aggregateFailures(failures, "All configured model routes failed.");
7848
7849
  return new LlmTransportError(error.code, aggregate.message, {
7849
7850
  retryable: aggregate.retryable,
7850
7851
  classification: classificationOf(aggregate),
@@ -7856,6 +7857,19 @@ function fallbackFailure(primary, error, route) {
7856
7857
  function isGeminiRoute(route) {
7857
7858
  return route.protocol === "google-gemini" || /(^|\/)gemini-/i.test(route.model);
7858
7859
  }
7860
+ function geminiRoutes(primary) {
7861
+ const prefix = primary.info.model.slice(0, primary.info.model.lastIndexOf("/") + 1);
7862
+ const models = /* @__PURE__ */ new Set([primary.info.model, ...GEMINI_FLASH_MODELS.map((model) => prefix + model)]);
7863
+ return [...models].map((model) => {
7864
+ if (model === primary.info.model) return primary;
7865
+ const modelConfig = { ...primary.modelConfig, model };
7866
+ return {
7867
+ ...primary,
7868
+ modelConfig,
7869
+ info: { ...primary.info, model, fingerprint: routeFingerprint(primary.info.task, primary.providerConfig, modelConfig) }
7870
+ };
7871
+ });
7872
+ }
7859
7873
  function canFallback(error, signal) {
7860
7874
  return !signal?.aborted && error instanceof LlmTransportError && error.code !== "cancelled";
7861
7875
  }
@@ -8162,25 +8176,43 @@ var ModelRouter = class {
8162
8176
  generate: async (request2) => {
8163
8177
  request2.signal?.throwIfAborted();
8164
8178
  if (request2.media.kind === "local-fallback") {
8165
- return this.generateDoubaoVideo(task, request2, request2.media.primaryFailure);
8179
+ return this.generateDoubaoVideo(task, request2, [request2.media.primaryFailure]);
8166
8180
  }
8167
- try {
8168
- return await this.tracked(route.info, () => adapter.generate({ ...request2, maxOutputTokens: spec2.maxOutputTokens }));
8169
- } catch (error) {
8170
- if (!canFallback(error, request2.signal) || !this.doubaoAdapter() || !request2.localPath) throw error;
8171
- return this.generateDoubaoVideo(task, request2, routedFailure(error, route.info));
8181
+ const routes = geminiRoutes(route);
8182
+ const failures = [];
8183
+ for (const [index, candidate] of routes.entries()) {
8184
+ request2.signal?.throwIfAborted();
8185
+ try {
8186
+ return await this.tracked(candidate.info, () => this.videoAdapter(candidate).generate({
8187
+ ...request2,
8188
+ maxOutputTokens: spec2.maxOutputTokens
8189
+ }));
8190
+ } catch (error) {
8191
+ if (!canFallback(error, request2.signal)) throw error;
8192
+ const failure = routedFailure(error, candidate.info);
8193
+ failures.push(failure);
8194
+ const next = routes[index + 1];
8195
+ if (next) {
8196
+ process.stderr.write(`\u21AA\uFE0F ${candidate.info.model} \u8BF7\u6C42\u5931\u8D25\uFF0C\u5207\u6362\u5230 ${next.info.model} (${formatFailure(failure)})
8197
+ `);
8198
+ continue;
8199
+ }
8200
+ if (!this.doubaoAdapter() || !request2.localPath) throw exhaustedFailure(error, failures);
8201
+ return this.generateDoubaoVideo(task, request2, failures);
8202
+ }
8172
8203
  }
8204
+ throw new Error("Gemini model routes cannot be empty.");
8173
8205
  }
8174
8206
  };
8175
8207
  }
8176
- async generateDoubaoVideo(task, request2, primary) {
8208
+ async generateDoubaoVideo(task, request2, failures) {
8177
8209
  const adapter = this.doubaoAdapter();
8178
8210
  if (!adapter) throw credentialError("doubao");
8179
8211
  const localPath = request2.media.kind === "local-fallback" ? request2.media.path : request2.localPath;
8180
8212
  if (!localPath) throw configRuntimeError("Doubao video fallback requires the local video.", ["localPath"], ["localPath: missing"]);
8181
8213
  const localMime = request2.localMime ?? (request2.media.kind === "file-api" ? request2.media.file.mimeType : request2.media.mimeType);
8182
8214
  const route = this.doubaoRoute(task);
8183
- process.stderr.write(`\u21AA\uFE0F ${primary.model} \u8BF7\u6C42\u5931\u8D25\uFF0C\u56DE\u9000\u5230\u8C46\u5305 ${route.model} (${formatFailure(primary)})
8215
+ process.stderr.write(`\u21AA\uFE0F Gemini \u8DEF\u7531\u5DF2\u8017\u5C3D\uFF0C\u56DE\u9000\u5230\u8C46\u5305 ${route.model}
8184
8216
  `);
8185
8217
  try {
8186
8218
  return await this.tracked(route, () => adapter.run({
@@ -8195,7 +8227,7 @@ var ModelRouter = class {
8195
8227
  }));
8196
8228
  } catch (error) {
8197
8229
  if (!canFallback(error, request2.signal)) throw error;
8198
- throw fallbackFailure(primary, error, route);
8230
+ throw exhaustedFailure(error, [...failures, routedFailure(error, route)]);
8199
8231
  }
8200
8232
  }
8201
8233
  // Resolve the default fallback or explicitly selected Doubao video.
@@ -8224,24 +8256,38 @@ var ModelRouter = class {
8224
8256
  }
8225
8257
  async withTextFallback(task, signal, generate) {
8226
8258
  const route = this.resolve(task);
8227
- const adapter = this.adapter(route);
8228
- try {
8229
- return await this.tracked(route.info, () => generate(adapter));
8230
- } catch (error) {
8231
- if (!canFallback(error, signal) || !isGeminiRoute(route.info) && error.code !== "content_filtered") throw error;
8232
- const doubao = this.doubaoTextAdapter();
8233
- if (!doubao) throw error;
8234
- const primary = routedFailure(error, route.info);
8235
- const fallback = this.doubaoRoute(task);
8236
- process.stderr.write(`\u21AA\uFE0F ${route.info.model} \u8BF7\u6C42\u5931\u8D25\uFF0C\u56DE\u9000\u5230\u8C46\u5305 ${fallback.model} (${formatFailure(primary)})
8237
- `);
8259
+ const gemini = isGeminiRoute(route.info);
8260
+ const routes = gemini ? geminiRoutes(route) : [route];
8261
+ const failures = [];
8262
+ for (const [index, candidate] of routes.entries()) {
8263
+ signal?.throwIfAborted();
8264
+ const adapter = this.adapter(candidate);
8238
8265
  try {
8239
- return await this.tracked(fallback, () => generate(doubao));
8240
- } catch (fallbackError) {
8241
- if (!canFallback(fallbackError, signal)) throw fallbackError;
8242
- throw fallbackFailure(primary, fallbackError, fallback);
8266
+ return await this.tracked(candidate.info, () => generate(adapter));
8267
+ } catch (error) {
8268
+ if (!canFallback(error, signal) || !gemini && error.code !== "content_filtered") throw error;
8269
+ const failure = routedFailure(error, candidate.info);
8270
+ failures.push(failure);
8271
+ const next = routes[index + 1];
8272
+ if (next) {
8273
+ process.stderr.write(`\u21AA\uFE0F ${candidate.info.model} \u8BF7\u6C42\u5931\u8D25\uFF0C\u5207\u6362\u5230 ${next.info.model} (${formatFailure(failure)})
8274
+ `);
8275
+ continue;
8276
+ }
8277
+ const doubao = this.doubaoTextAdapter();
8278
+ if (!doubao) throw exhaustedFailure(error, failures);
8279
+ const fallback = this.doubaoRoute(task);
8280
+ process.stderr.write(`\u21AA\uFE0F \u4E3B\u6A21\u578B\u8DEF\u7531\u5DF2\u8017\u5C3D\uFF0C\u56DE\u9000\u5230\u8C46\u5305 ${fallback.model}
8281
+ `);
8282
+ try {
8283
+ return await this.tracked(fallback, () => generate(doubao));
8284
+ } catch (fallbackError) {
8285
+ if (!canFallback(fallbackError, signal)) throw fallbackError;
8286
+ throw exhaustedFailure(fallbackError, [...failures, routedFailure(fallbackError, fallback)]);
8287
+ }
8243
8288
  }
8244
8289
  }
8290
+ throw new Error("Text model routes cannot be empty.");
8245
8291
  }
8246
8292
  async generateText(task, request2) {
8247
8293
  const spec2 = LLM_TASK_REGISTRY[task];