@jaypie/llm 1.2.31 → 1.2.33

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.
@@ -13,6 +13,10 @@ import { BaseProviderAdapter } from "./ProviderAdapter.interface.js";
13
13
  export declare class OpenAiAdapter extends BaseProviderAdapter {
14
14
  readonly name: "openai";
15
15
  readonly defaultModel: "gpt-5.4";
16
+ private runtimeNoTemperatureModels;
17
+ rememberModelRejectsTemperature(model: string): void;
18
+ clearRuntimeNoTemperatureModels(): void;
19
+ private supportsTemperature;
16
20
  buildRequest(request: OperateRequest): unknown;
17
21
  formatTools(toolkit: Toolkit, _outputSchema?: JsonObject): ProviderToolDefinition[];
18
22
  formatOutputSchema(schema: JsonObject | NaturalSchema | z.ZodType): JsonObject;
@@ -82,11 +82,26 @@ interface OpenRouterRequest {
82
82
  user?: string;
83
83
  }
84
84
  /**
85
- * OpenRouter content part types (text only - images/files not supported)
85
+ * OpenRouter content part types. OpenRouter follows the OpenAI Chat
86
+ * Completions multimodal schema and forwards `image_url` and `file` parts
87
+ * to vision/PDF-capable backends. The SDK accepts camelCase fields at the
88
+ * input boundary (`imageUrl`, `fileData`) and transforms to snake_case on
89
+ * the wire.
86
90
  */
87
91
  type OpenRouterContentPart = {
88
92
  type: "text";
89
93
  text: string;
94
+ } | {
95
+ type: "image_url";
96
+ imageUrl: {
97
+ url: string;
98
+ };
99
+ } | {
100
+ type: "file";
101
+ file: {
102
+ filename?: string;
103
+ fileData: string;
104
+ };
90
105
  };
91
106
  /**
92
107
  * OpenRouterAdapter implements the ProviderAdapter interface for OpenRouter's API.
@@ -101,6 +116,10 @@ export declare class OpenRouterAdapter extends BaseProviderAdapter {
101
116
  rememberModelRejectsStructuredOutput(model: string): void;
102
117
  clearRuntimeNoStructuredOutputModels(): void;
103
118
  private supportsStructuredOutput;
119
+ private runtimeNoTemperatureModels;
120
+ rememberModelRejectsTemperature(model: string): void;
121
+ clearRuntimeNoTemperatureModels(): void;
122
+ private supportsTemperature;
104
123
  buildRequest(request: OperateRequest): OpenRouterRequest;
105
124
  formatTools(toolkit: Toolkit, _outputSchema?: JsonObject): ProviderToolDefinition[];
106
125
  formatOutputSchema(schema: JsonObject | NaturalSchema | z.ZodType): JsonObject;
package/dist/esm/index.js CHANGED
@@ -1197,11 +1197,11 @@ const NOT_RETRYABLE_ERROR_NAMES = [
1197
1197
  // Patterns (not exact names) so dated variants and future releases are covered
1198
1198
  // without code changes — Anthropic is trending toward removing temperature on
1199
1199
  // newer Claude models.
1200
- const MODELS_WITHOUT_TEMPERATURE = [
1200
+ const MODELS_WITHOUT_TEMPERATURE$2 = [
1201
1201
  /^claude-opus-4-[789]/,
1202
1202
  /^claude-opus-[5-9]/,
1203
1203
  ];
1204
- function isTemperatureDeprecationError(error) {
1204
+ function isTemperatureDeprecationError$2(error) {
1205
1205
  if (!error || typeof error !== "object")
1206
1206
  return false;
1207
1207
  const err = error;
@@ -1271,7 +1271,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
1271
1271
  supportsTemperature(model) {
1272
1272
  if (this.runtimeNoTemperatureModels.has(model))
1273
1273
  return false;
1274
- return !MODELS_WITHOUT_TEMPERATURE.some((pattern) => pattern.test(model));
1274
+ return !MODELS_WITHOUT_TEMPERATURE$2.some((pattern) => pattern.test(model));
1275
1275
  }
1276
1276
  supportsStructuredOutput(model) {
1277
1277
  return !this.runtimeNoStructuredOutputModels.has(model);
@@ -1449,7 +1449,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
1449
1449
  return undefined;
1450
1450
  // If the model rejected `temperature`, cache it and retry without the param
1451
1451
  if (anthropicRequest.temperature !== undefined &&
1452
- isTemperatureDeprecationError(error)) {
1452
+ isTemperatureDeprecationError$2(error)) {
1453
1453
  this.rememberModelRejectsTemperature(anthropicRequest.model);
1454
1454
  const retryRequest = { ...anthropicRequest };
1455
1455
  delete retryRequest.temperature;
@@ -1510,7 +1510,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
1510
1510
  }
1511
1511
  catch (error) {
1512
1512
  if (streamRequest.temperature !== undefined &&
1513
- isTemperatureDeprecationError(error)) {
1513
+ isTemperatureDeprecationError$2(error)) {
1514
1514
  this.rememberModelRejectsTemperature(streamRequest.model);
1515
1515
  streamRequest = {
1516
1516
  ...streamRequest,
@@ -2237,14 +2237,24 @@ class GeminiAdapter extends BaseProviderAdapter {
2237
2237
  // Tool Result Handling
2238
2238
  //
2239
2239
  formatToolResult(toolCall, result) {
2240
- // Gemini expects the response to be the actual result object, not wrapped in "result"
2241
- // The output from StandardToolResult is JSON-stringified, so we need to parse it
2240
+ // Gemini's `function_response.response` is a protobuf Struct, which only
2241
+ // accepts plain objects. Some models (e.g. gemini-3.1-pro) accept scalar
2242
+ // values silently; gemini-3.1-flash-lite rejects them. Parse the output
2243
+ // and wrap any non-object value (string, number, boolean, array, null)
2244
+ // in `{ result: value }` so every tool result is a valid Struct.
2242
2245
  let responseData;
2243
2246
  try {
2244
- responseData = JSON.parse(result.output);
2247
+ const parsed = JSON.parse(result.output);
2248
+ if (parsed !== null &&
2249
+ typeof parsed === "object" &&
2250
+ !Array.isArray(parsed)) {
2251
+ responseData = parsed;
2252
+ }
2253
+ else {
2254
+ responseData = { result: parsed };
2255
+ }
2245
2256
  }
2246
2257
  catch {
2247
- // If parsing fails, wrap the output as a string result
2248
2258
  responseData = { result: result.output };
2249
2259
  }
2250
2260
  return {
@@ -2661,6 +2671,27 @@ const NOT_RETRYABLE_ERROR_TYPES = [
2661
2671
  RateLimitError,
2662
2672
  UnprocessableEntityError,
2663
2673
  ];
2674
+ // Models known not to accept `temperature`.
2675
+ // Patterns (not exact names) so dated variants and future releases are covered
2676
+ // without code changes — OpenAI is removing temperature on newer reasoning
2677
+ // models.
2678
+ const MODELS_WITHOUT_TEMPERATURE$1 = [
2679
+ /^gpt-5\.5/, // gpt-5.5 series deprecated temperature
2680
+ /^o\d/, // o-series reasoning models (o1, o3, o4, ...)
2681
+ ];
2682
+ function isTemperatureDeprecationError$1(error) {
2683
+ if (!error || typeof error !== "object")
2684
+ return false;
2685
+ if (!(error instanceof BadRequestError) &&
2686
+ error.status !== 400) {
2687
+ return false;
2688
+ }
2689
+ const messages = [
2690
+ error.message,
2691
+ error.error?.message,
2692
+ ].filter((m) => typeof m === "string");
2693
+ return messages.some((m) => m.toLowerCase().includes("temperature"));
2694
+ }
2664
2695
  //
2665
2696
  //
2666
2697
  // Main
@@ -2675,6 +2706,20 @@ class OpenAiAdapter extends BaseProviderAdapter {
2675
2706
  super(...arguments);
2676
2707
  this.name = PROVIDER.OPENAI.NAME;
2677
2708
  this.defaultModel = PROVIDER.OPENAI.MODEL.DEFAULT;
2709
+ // Session-level cache of models observed to reject `temperature` at runtime.
2710
+ // Populated by executeRequest on 400 errors so repeat calls skip the param.
2711
+ this.runtimeNoTemperatureModels = new Set();
2712
+ }
2713
+ rememberModelRejectsTemperature(model) {
2714
+ this.runtimeNoTemperatureModels.add(model);
2715
+ }
2716
+ clearRuntimeNoTemperatureModels() {
2717
+ this.runtimeNoTemperatureModels.clear();
2718
+ }
2719
+ supportsTemperature(model) {
2720
+ if (this.runtimeNoTemperatureModels.has(model))
2721
+ return false;
2722
+ return !MODELS_WITHOUT_TEMPERATURE$1.some((pattern) => pattern.test(model));
2678
2723
  }
2679
2724
  //
2680
2725
  // Request Building
@@ -2718,6 +2763,11 @@ class OpenAiAdapter extends BaseProviderAdapter {
2718
2763
  if (request.temperature !== undefined) {
2719
2764
  openaiRequest.temperature = request.temperature;
2720
2765
  }
2766
+ // Strip temperature for models that don't support it (denylist + runtime cache)
2767
+ if (openaiRequest.temperature !== undefined &&
2768
+ !this.supportsTemperature(openaiRequest.model)) {
2769
+ delete openaiRequest.temperature;
2770
+ }
2721
2771
  return openaiRequest;
2722
2772
  }
2723
2773
  formatTools(toolkit, _outputSchema) {
@@ -2767,14 +2817,21 @@ class OpenAiAdapter extends BaseProviderAdapter {
2767
2817
  //
2768
2818
  async executeRequest(client, request, signal) {
2769
2819
  const openai = client;
2820
+ const openaiRequest = request;
2770
2821
  try {
2771
- return await openai.responses.create(
2772
- // @ts-expect-error OpenAI SDK types don't match our request format exactly
2773
- request, signal ? { signal } : undefined);
2822
+ return await openai.responses.create(openaiRequest, signal ? { signal } : undefined);
2774
2823
  }
2775
2824
  catch (error) {
2776
2825
  if (signal?.aborted)
2777
2826
  return undefined;
2827
+ // If the model rejected `temperature`, cache it and retry without the param
2828
+ if (openaiRequest.temperature !== undefined &&
2829
+ isTemperatureDeprecationError$1(error)) {
2830
+ this.rememberModelRejectsTemperature(openaiRequest.model);
2831
+ const retryRequest = { ...openaiRequest };
2832
+ delete retryRequest.temperature;
2833
+ return await openai.responses.create(retryRequest, signal ? { signal } : undefined);
2834
+ }
2778
2835
  throw error;
2779
2836
  }
2780
2837
  }
@@ -3119,10 +3176,31 @@ function isStructuredOutputUnsupportedError(error) {
3119
3176
  const messages = [err.message, err.error?.message].filter((m) => typeof m === "string");
3120
3177
  return messages.some((m) => /response_format|json[_ ]schema|structured[_ ]output|require[_ ]parameters/i.test(m));
3121
3178
  }
3179
+ // OpenRouter routes that don't accept `temperature`. Patterns match the
3180
+ // vendor-prefixed route id (e.g. `openai/gpt-5.5`) so dated variants are
3181
+ // covered without code changes.
3182
+ const MODELS_WITHOUT_TEMPERATURE = [
3183
+ /^openai\/gpt-5\.5/,
3184
+ /^openai\/o\d/,
3185
+ /^anthropic\/claude-opus-4-[789]/,
3186
+ /^anthropic\/claude-opus-[5-9]/,
3187
+ ];
3188
+ function isTemperatureDeprecationError(error) {
3189
+ if (!error || typeof error !== "object")
3190
+ return false;
3191
+ const err = error;
3192
+ const status = err.status ?? err.statusCode;
3193
+ if (status !== 400)
3194
+ return false;
3195
+ const messages = [err.message, err.error?.message].filter((m) => typeof m === "string");
3196
+ return messages.some((m) => m.toLowerCase().includes("temperature"));
3197
+ }
3122
3198
  /**
3123
- * Convert standardized content items to OpenRouter format
3124
- * Note: OpenRouter does not support native file/image uploads.
3125
- * Images and files are discarded with a warning.
3199
+ * Convert standardized content items to OpenRouter format. Images become
3200
+ * `image_url` parts and files become `file` parts; both pass through to
3201
+ * OpenRouter which routes to the selected backend. Backends that don't
3202
+ * support the modality 4xx — that's a model-capability mismatch, surfaced
3203
+ * by the call rather than silently dropped here.
3126
3204
  */
3127
3205
  function convertContentToOpenRouter(content) {
3128
3206
  if (typeof content === "string") {
@@ -3130,25 +3208,38 @@ function convertContentToOpenRouter(content) {
3130
3208
  }
3131
3209
  const parts = [];
3132
3210
  for (const item of content) {
3133
- // Text content - pass through
3134
3211
  if (item.type === LlmMessageType.InputText) {
3135
3212
  parts.push({ type: "text", text: item.text });
3136
3213
  continue;
3137
3214
  }
3138
- // Image content - warn and discard
3139
3215
  if (item.type === LlmMessageType.InputImage) {
3140
- log$1.warn("OpenRouter does not support image uploads; image discarded");
3216
+ const url = item.image_url ?? "";
3217
+ if (!url) {
3218
+ log$1.warn("OpenRouter image content missing image_url; image discarded");
3219
+ continue;
3220
+ }
3221
+ parts.push({ type: "image_url", imageUrl: { url } });
3141
3222
  continue;
3142
3223
  }
3143
- // File/Document content - warn and discard
3144
3224
  if (item.type === LlmMessageType.InputFile) {
3145
- log$1.warn({ filename: item.filename }, "OpenRouter does not support file uploads; file discarded");
3225
+ const fileData = typeof item.file_data === "string" ? item.file_data : "";
3226
+ if (!fileData) {
3227
+ log$1.warn({ filename: item.filename }, "OpenRouter file content missing file_data; file discarded");
3228
+ continue;
3229
+ }
3230
+ parts.push({
3231
+ type: "file",
3232
+ file: {
3233
+ filename: item.filename,
3234
+ fileData,
3235
+ },
3236
+ });
3146
3237
  continue;
3147
3238
  }
3148
3239
  // Unknown type - warn and skip
3149
3240
  log$1.warn({ item }, "Unknown content type for OpenRouter; discarded");
3150
3241
  }
3151
- // If no text parts remain, return empty string to avoid empty array
3242
+ // If no parts remain, return empty string to avoid empty array
3152
3243
  if (parts.length === 0) {
3153
3244
  return "";
3154
3245
  }
@@ -3202,6 +3293,9 @@ class OpenRouterAdapter extends BaseProviderAdapter {
3202
3293
  // `response_format: json_schema`. When a model is in this set, buildRequest
3203
3294
  // engages the legacy fake-tool path instead of native structured output.
3204
3295
  this.runtimeNoStructuredOutputModels = new Set();
3296
+ // Session-level cache of routes observed to reject `temperature`. Populated
3297
+ // by executeRequest on 400 errors so repeat calls skip the param.
3298
+ this.runtimeNoTemperatureModels = new Set();
3205
3299
  }
3206
3300
  rememberModelRejectsStructuredOutput(model) {
3207
3301
  this.runtimeNoStructuredOutputModels.add(model);
@@ -3212,6 +3306,17 @@ class OpenRouterAdapter extends BaseProviderAdapter {
3212
3306
  supportsStructuredOutput(model) {
3213
3307
  return !this.runtimeNoStructuredOutputModels.has(model);
3214
3308
  }
3309
+ rememberModelRejectsTemperature(model) {
3310
+ this.runtimeNoTemperatureModels.add(model);
3311
+ }
3312
+ clearRuntimeNoTemperatureModels() {
3313
+ this.runtimeNoTemperatureModels.clear();
3314
+ }
3315
+ supportsTemperature(model) {
3316
+ if (this.runtimeNoTemperatureModels.has(model))
3317
+ return false;
3318
+ return !MODELS_WITHOUT_TEMPERATURE.some((pattern) => pattern.test(model));
3319
+ }
3215
3320
  //
3216
3321
  // Request Building
3217
3322
  //
@@ -3281,6 +3386,12 @@ class OpenRouterAdapter extends BaseProviderAdapter {
3281
3386
  openRouterRequest.temperature =
3282
3387
  request.temperature;
3283
3388
  }
3389
+ // Strip temperature for routes that don't support it (denylist + runtime cache)
3390
+ const requestRecord = openRouterRequest;
3391
+ if (requestRecord.temperature !== undefined &&
3392
+ !this.supportsTemperature(openRouterRequest.model)) {
3393
+ delete requestRecord.temperature;
3394
+ }
3284
3395
  return openRouterRequest;
3285
3396
  }
3286
3397
  formatTools(toolkit,
@@ -3307,11 +3418,18 @@ class OpenRouterAdapter extends BaseProviderAdapter {
3307
3418
  jsonSchema.type = "object"; // Normalize type
3308
3419
  }
3309
3420
  else {
3310
- // Convert NaturalSchema to JSON schema through Zod
3421
+ // Convert NaturalSchema to JSON schema through Zod. Re-spread into a
3422
+ // plain object: Zod v4's z.toJSONSchema returns an object that carries
3423
+ // a non-configurable `~standard` Standard-Schema interop marker as a
3424
+ // non-enumerable own property. Anthropic's output_config.format
3425
+ // validation is strict and rejects unknown properties when OpenRouter
3426
+ // forwards the schema, so we drop the marker here. JSON.stringify
3427
+ // already skips it but the OpenRouter SDK enumerates own properties
3428
+ // during serialization.
3311
3429
  const zodSchema = schema instanceof z.ZodType
3312
3430
  ? schema
3313
3431
  : naturalZodSchema(schema);
3314
- jsonSchema = z.toJSONSchema(zodSchema);
3432
+ jsonSchema = { ...z.toJSONSchema(zodSchema) };
3315
3433
  }
3316
3434
  // Remove $schema property (can cause issues with some providers)
3317
3435
  if (jsonSchema.$schema) {
@@ -3339,6 +3457,20 @@ class OpenRouterAdapter extends BaseProviderAdapter {
3339
3457
  catch (error) {
3340
3458
  if (signal?.aborted)
3341
3459
  return undefined;
3460
+ // If the route rejected `temperature` (e.g., openai/gpt-5.5 forwarding),
3461
+ // cache it and retry without the param.
3462
+ const requestRecord = openRouterRequest;
3463
+ if (requestRecord.temperature !== undefined &&
3464
+ isTemperatureDeprecationError(error)) {
3465
+ this.rememberModelRejectsTemperature(openRouterRequest.model);
3466
+ const retryRequest = { ...openRouterRequest };
3467
+ delete retryRequest.temperature;
3468
+ const response = (await openRouter.chat.send(this.toSdkChatParams(retryRequest), signal ? { signal } : undefined));
3469
+ if (wantsStructuredOutput) {
3470
+ response.__jaypieStructuredOutput = true;
3471
+ }
3472
+ return response;
3473
+ }
3342
3474
  // If the model rejected `response_format`, cache it and retry with the
3343
3475
  // legacy fake-tool emulation path.
3344
3476
  if (wantsStructuredOutput && isStructuredOutputUnsupportedError(error)) {