@kortyx/openrouter 0.2.5 → 0.2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # @kortyx/openrouter
2
2
 
3
+ ## Unreleased
4
+
5
+ ### Bug Fixes
6
+
7
+ - Report confirmed parameter-routing 404s as `PROVIDER_PARAMETER_UNSUPPORTED`,
8
+ including requested parameter names, tool/output versus other controls and
9
+ the automatic/explicit/default source of `requireParameters`. Explain the
10
+ existing explicit override and preset visibility limits. Preserve original
11
+ developer-facing messages and private SDK causes; recognize numeric/string
12
+ codes and structured routing evidence. Surface malformed SDK responses as
13
+ `PROVIDER_INVALID_RESPONSE` with partial usage. Keep request defaults and
14
+ automatic retry behavior unchanged.
15
+
16
+
17
+ ## [0.2.6](https://github.com/kortyx-io/kortyx/compare/openrouter-v0.2.5...openrouter-v0.2.6) (2026-10-13)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * **openrouter:** explain parameter routing failures ([#338](https://github.com/kortyx-io/kortyx/issues/338)) ([193a287](https://github.com/kortyx-io/kortyx/commit/193a28794b6f05626a2d69148fa8592fcd5956e9))
23
+
24
+
25
+ ### Dependencies
26
+
27
+ * The following workspace dependencies were updated
28
+ * dependencies
29
+ * @kortyx/core bumped to 0.14.0
30
+ * @kortyx/providers bumped to 0.9.3
31
+
3
32
  ## [0.2.5](https://github.com/kortyx-io/kortyx/compare/openrouter-v0.2.4...openrouter-v0.2.5) (2026-10-11)
4
33
 
5
34
 
package/README.md CHANGED
@@ -38,6 +38,45 @@ OpenRouter-specific request controls belong under
38
38
  `provider.requireParameters` when a request uses function tools or a native
39
39
  JSON schema, unless the caller explicitly chooses another value.
40
40
 
41
+ This is an all-parameter requirement: optional controls such as `temperature`
42
+ must also be supported by the selected endpoint. The default is unchanged.
43
+
44
+ Parameter-routing refusals use `PROVIDER_PARAMETER_UNSUPPORTED`. The safe failure
45
+ explains whether `requireParameters` was enabled automatically for tools/native
46
+ JSON schemas, explicitly configured by the caller, or left at the provider default.
47
+ It lists tool/output parameters (`response_format`, `tools`, `tool_choice`)
48
+ separately from other requested controls. These lists identify what was sent;
49
+ they do not claim which parameter caused the rejection or which controls your
50
+ application can safely omit.
51
+
52
+ Remove controls your application can omit, or choose a compatible route. The
53
+ error names this existing, explicit override when Kortyx enabled strict routing:
54
+
55
+ ```ts
56
+ const model = openrouter("anthropic/claude-sonnet-4.6", {
57
+ providerOptions: {
58
+ openrouter: { provider: { requireParameters: false } },
59
+ },
60
+ });
61
+ ```
62
+
63
+ With that override, unsupported controls may be ignored, including tools and
64
+ structured output. Kortyx does not remove parameters or retry a modified request.
65
+
66
+ Preset aliases can add settings on OpenRouter's side. The diagnostic warns that
67
+ its list only covers parameters sent by Kortyx; it does not fetch or change
68
+ presets, publish customer-chosen preset/model names, or identify a requested alias
69
+ as the resolved model.
70
+
71
+ For developer debugging, the live exception preserves the original provider
72
+ message and original SDK error as `cause`, including its status, stack and
73
+ metadata. Public failure descriptors contain the safe explanation and allowlisted
74
+ parameter names, excluding prompts, parameter values and account routing data.
75
+ Numeric/string 404 codes and structured parameter-filter evidence are recognized;
76
+ unrelated 404s retain their existing classification. If the SDK rejects a malformed
77
+ response and no parameter refusal can be established, `PROVIDER_INVALID_RESPONSE`
78
+ retains the SDK parsing error. Partial stream usage is preserved.
79
+
41
80
  ```ts
42
81
  const model = openrouter("anthropic/claude-sonnet-4.6", {
43
82
  providerOptions: {
package/dist/index.cjs CHANGED
@@ -5412,6 +5412,16 @@ function createOpenRouterClient(settings, apiKey) {
5412
5412
  // src/errors.ts
5413
5413
  var import_errors = require("@kortyx/core/errors");
5414
5414
  var import_errors2 = require("@kortyx/core/errors");
5415
+ var rawEnvelope = (error) => {
5416
+ const raw = (0, import_errors.errorProperty)(error, "rawValue");
5417
+ if (typeof raw !== "string") return raw;
5418
+ if (raw.length > import_errors.FAILURE_LIMITS.bytes) return void 0;
5419
+ try {
5420
+ return JSON.parse(raw);
5421
+ } catch {
5422
+ return void 0;
5423
+ }
5424
+ };
5415
5425
  var requireApiKey = (apiKey) => {
5416
5426
  if (!apiKey || apiKey.trim().length === 0) {
5417
5427
  throw new import_errors.ProviderConfigurationError(
@@ -5420,7 +5430,115 @@ var requireApiKey = (apiKey) => {
5420
5430
  }
5421
5431
  return apiKey;
5422
5432
  };
5423
- var toProviderRequestError = (action, error) => (0, import_errors.normalizeProviderError)("openrouter", action, error);
5433
+ var parameterNames = (request) => {
5434
+ const parameters = {
5435
+ temperature: request.temperature,
5436
+ top_p: request.topP,
5437
+ top_k: request.topK,
5438
+ frequency_penalty: request.frequencyPenalty,
5439
+ presence_penalty: request.presencePenalty,
5440
+ repetition_penalty: request.repetitionPenalty,
5441
+ min_p: request.minP,
5442
+ top_a: request.topA,
5443
+ stop: request.stop,
5444
+ seed: request.seed,
5445
+ max_tokens: request.maxTokens,
5446
+ max_completion_tokens: request.maxCompletionTokens,
5447
+ reasoning: request.reasoning,
5448
+ response_format: request.responseFormat,
5449
+ tools: request.tools,
5450
+ tool_choice: request.toolChoice,
5451
+ logprobs: request.logprobs,
5452
+ top_logprobs: request.topLogprobs,
5453
+ logit_bias: request.logitBias,
5454
+ parallel_tool_calls: request.parallelToolCalls,
5455
+ prediction: request.prediction,
5456
+ modalities: request.modalities,
5457
+ image_config: request.imageConfig,
5458
+ reasoning_effort: request.reasoningEffort
5459
+ };
5460
+ return Object.entries(parameters).filter(([, value]) => value !== void 0).map(([name]) => name);
5461
+ };
5462
+ var toProviderRequestError = (action, error, request, context) => {
5463
+ if (request && !(0, import_errors.isControlFlowError)(error)) {
5464
+ const envelope = rawEnvelope(error);
5465
+ const detail = (0, import_errors.errorProperty)(error, "error") ?? (0, import_errors.errorProperty)(envelope, "error") ?? error;
5466
+ const metadata2 = (0, import_errors.errorProperty)(detail, "metadata");
5467
+ const routing = (0, import_errors.errorProperty)(metadata2, "routing_funnel") ?? (0, import_errors.errorProperty)(
5468
+ (0, import_errors.errorProperty)(error, "openrouterMetadata") ?? (0, import_errors.errorProperty)(error, "openrouter_metadata") ?? (0, import_errors.errorProperty)(envelope, "openrouter_metadata"),
5469
+ "routing_funnel"
5470
+ );
5471
+ const failedRoutingStep = (0, import_errors.errorProperty)(routing, "failed_routing_step") ?? (0, import_errors.errorProperty)(metadata2, "failed_routing_step");
5472
+ const rawStatus = (0, import_errors.errorProperty)(error, "statusCode") ?? (0, import_errors.errorProperty)(detail, "code");
5473
+ const status = typeof rawStatus === "number" || typeof rawStatus === "string" ? Number(rawStatus) : void 0;
5474
+ const message = (0, import_errors.errorProperty)(detail, "message") ?? (0, import_errors.errorProperty)(error, "message");
5475
+ const parameterFilter = typeof failedRoutingStep === "string" && failedRoutingStep.trim().toLowerCase() === "filter by parameters";
5476
+ const parameterMessage = typeof message === "string" && /^no endpoints found that can handle the requested parameters\.?$/i.test(
5477
+ message.trim()
5478
+ );
5479
+ if (status === 404 && (parameterFilter || failedRoutingStep === void 0 && parameterMessage)) {
5480
+ const requestedParameters = parameterNames(request);
5481
+ const requireParameters = request.provider?.requireParameters ?? false;
5482
+ const requireParametersSource = context?.requireParametersSource ?? "unknown";
5483
+ const featureNames = /* @__PURE__ */ new Set(["response_format", "tools", "tool_choice"]);
5484
+ const featureParameters = requestedParameters.filter(
5485
+ (name2) => featureNames.has(name2)
5486
+ );
5487
+ const otherParameters = requestedParameters.filter(
5488
+ (name2) => !featureNames.has(name2)
5489
+ );
5490
+ const usesPreset = Boolean(
5491
+ request.model?.includes("@preset/") || request.models?.some((model) => model.includes("@preset/"))
5492
+ );
5493
+ const automaticFeatures = [
5494
+ ...request.tools?.length ? ["tool calling"] : [],
5495
+ ...request.responseFormat?.type === "json_schema" ? ["structured output"] : []
5496
+ ];
5497
+ const origin = requireParametersSource === "automatic" ? `enabled automatically by Kortyx for ${automaticFeatures.join(" and ")}` : requireParametersSource === "explicit" ? "configured explicitly by the caller" : requireParametersSource === "default" ? "not explicitly set; provider default" : "setting origin unavailable";
5498
+ const guidance = "Remove unsupported optional parameters only if your application can omit them, or select a compatible model/provider route.";
5499
+ const override = requireParametersSource === "automatic" ? " To override strict parameter routing, set providerOptions.openrouter.provider.requireParameters=false; unsupported controls, including tools/structured output, may then be ignored." : "";
5500
+ const preset = usesPreset ? " Preset settings may also affect routing; this list only covers parameters sent by Kortyx." : "";
5501
+ const diagnostic = `OpenRouter found no endpoint supporting the requested parameters. require_parameters=${requireParameters} (${origin}). Tool/output parameters: ${featureParameters.join(", ") || "none"}. Other requested parameters: ${otherParameters.join(", ") || "none"}. ${guidance}${override}${preset}`;
5502
+ return new import_errors.ProviderRequestError(
5503
+ typeof message === "string" ? message : diagnostic,
5504
+ {
5505
+ code: "PROVIDER_PARAMETER_UNSUPPORTED",
5506
+ source: "openrouter",
5507
+ operation: action,
5508
+ status: 404,
5509
+ retryable: false,
5510
+ safeMessage: diagnostic,
5511
+ details: {
5512
+ requestedParameters,
5513
+ requireParameters,
5514
+ requireParametersSource,
5515
+ featureParameters,
5516
+ otherParameters,
5517
+ usesPreset,
5518
+ ...parameterFilter ? { failedRoutingStep: "Filter by Parameters" } : {}
5519
+ },
5520
+ cause: error
5521
+ }
5522
+ );
5523
+ }
5524
+ }
5525
+ const name = (0, import_errors.errorProperty)(error, "name");
5526
+ const originalMessage = (0, import_errors.errorProperty)(error, "message");
5527
+ if (name === "ResponseValidationError" || context?.streamStarted && (name === "SDKValidationError" || name === "ZodError")) {
5528
+ return new import_errors.ProviderRequestError(
5529
+ typeof originalMessage === "string" ? originalMessage : "OpenRouter SDK response validation failed.",
5530
+ {
5531
+ code: "PROVIDER_INVALID_RESPONSE",
5532
+ source: "openrouter",
5533
+ operation: action,
5534
+ retryable: false,
5535
+ safeMessage: "OpenRouter returned a response that did not match its SDK schema. Inspect the original SDK error for details.",
5536
+ cause: error
5537
+ }
5538
+ );
5539
+ }
5540
+ return (0, import_errors.normalizeProviderError)("openrouter", action, error);
5541
+ };
5424
5542
 
5425
5543
  // src/messages.ts
5426
5544
  var isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
@@ -5485,9 +5603,10 @@ var toTool = (tool) => ({
5485
5603
  strict: false
5486
5604
  }
5487
5605
  });
5488
- var createChatRequest = (modelId, messages, options, stream) => {
5606
+ var prepareChatRequest = (modelId, messages, options, stream) => {
5489
5607
  const extra = getOpenRouterOptions(options);
5490
5608
  const needsRequiredParameters = Boolean(options.tools?.length) || options.responseFormat?.type === "json" && options.responseFormat.schema !== void 0;
5609
+ const requireParametersSource = extra.provider?.requireParameters !== void 0 ? "explicit" : needsRequiredParameters ? "automatic" : "default";
5491
5610
  const provider = {
5492
5611
  ...extra.provider,
5493
5612
  ...needsRequiredParameters && extra.provider?.requireParameters === void 0 ? { requireParameters: true } : {}
@@ -5499,7 +5618,7 @@ var createChatRequest = (modelId, messages, options, stream) => {
5499
5618
  } : {},
5500
5619
  ...options.reasoning.includeThoughts ? { summary: "auto" } : {}
5501
5620
  } : extra.reasoning;
5502
- return {
5621
+ const request = {
5503
5622
  ...extra,
5504
5623
  model: modelId,
5505
5624
  messages: toMessages(messages),
@@ -5521,6 +5640,7 @@ var createChatRequest = (modelId, messages, options, stream) => {
5521
5640
  ...options.tools?.length ? { tools: options.tools.map(toTool) } : {},
5522
5641
  ...Object.keys(provider).length > 0 ? { provider } : {}
5523
5642
  };
5643
+ return { request, context: { requireParametersSource } };
5524
5644
  };
5525
5645
 
5526
5646
  // src/models.ts
@@ -5849,18 +5969,23 @@ var createChatModel = (modelId, settings, options) => {
5849
5969
  return {
5850
5970
  supportsToolStreaming: true,
5851
5971
  async invoke(messages) {
5972
+ let request;
5973
+ let context;
5852
5974
  try {
5853
- const response = await getClient().chat(
5854
- createChatRequest(modelId, messages, options, false),
5855
- options.abortSignal
5856
- );
5975
+ ({ request, context } = prepareChatRequest(
5976
+ modelId,
5977
+ messages,
5978
+ options,
5979
+ false
5980
+ ));
5981
+ const response = await getClient().chat(request, options.abortSignal);
5857
5982
  if (isAsyncIterable(response))
5858
5983
  throw new import_errors2.ProviderRequestError(
5859
5984
  "OpenRouter unexpectedly returned a stream for a non-streaming request."
5860
5985
  );
5861
5986
  return resultFrom(modelId, response, warnings);
5862
5987
  } catch (error) {
5863
- throw toProviderRequestError("invoke content", error);
5988
+ throw toProviderRequestError("invoke content", error, request, context);
5864
5989
  }
5865
5990
  },
5866
5991
  async *stream(messages) {
@@ -5881,15 +6006,21 @@ var createChatModel = (modelId, settings, options) => {
5881
6006
  }
5882
6007
  let lastChunk;
5883
6008
  let partialUsage;
6009
+ let request;
6010
+ let context;
5884
6011
  try {
5885
- const response = await getClient().chat(
5886
- createChatRequest(modelId, messages, options, true),
5887
- options.abortSignal
5888
- );
6012
+ ({ request, context } = prepareChatRequest(
6013
+ modelId,
6014
+ messages,
6015
+ options,
6016
+ true
6017
+ ));
6018
+ const response = await getClient().chat(request, options.abortSignal);
5889
6019
  if (!isAsyncIterable(response))
5890
6020
  throw new import_errors2.ProviderRequestError(
5891
6021
  "OpenRouter unexpectedly returned a non-streaming response for a streaming request."
5892
6022
  );
6023
+ context.streamStarted = true;
5893
6024
  const calls = /* @__PURE__ */ new Map();
5894
6025
  const reasoningDetails = [];
5895
6026
  let finishReason;
@@ -5897,9 +6028,12 @@ var createChatModel = (modelId, settings, options) => {
5897
6028
  lastChunk = chunk;
5898
6029
  partialUsage = mapUsage2(chunk.usage) ?? partialUsage;
5899
6030
  if (chunk.error)
5900
- throw new import_errors2.ProviderRequestError(chunk.error.message, {
5901
- ...chunk.error.metadata ? { details: chunk.error.metadata } : {}
5902
- });
6031
+ throw toProviderRequestError(
6032
+ "stream content",
6033
+ chunk.error,
6034
+ request,
6035
+ context
6036
+ );
5903
6037
  for (const choice of chunk.choices) {
5904
6038
  finishReason = choice.finishReason ?? finishReason;
5905
6039
  if (choice.delta.content)
@@ -5962,7 +6096,7 @@ var createChatModel = (modelId, settings, options) => {
5962
6096
  yield {
5963
6097
  type: "error",
5964
6098
  error: Object.assign(
5965
- toProviderRequestError("stream content", error),
6099
+ toProviderRequestError("stream content", error, request, context),
5966
6100
  {
5967
6101
  usage: partialUsage
5968
6102
  }