@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 +29 -0
- package/README.md +39 -0
- package/dist/index.cjs +150 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +165 -27
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
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
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
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
|
|
5901
|
-
|
|
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
|
}
|