@jaypie/llm 1.3.3 → 1.3.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/dist/cjs/index.cjs +741 -119
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/operate/adapters/AnthropicAdapter.d.ts +1 -1
- package/dist/cjs/operate/adapters/OpenRouterAdapter.d.ts +8 -1
- package/dist/cjs/providers/anthropic/client.d.ts +54 -0
- package/dist/cjs/providers/anthropic/types.d.ts +127 -0
- package/dist/cjs/providers/anthropic/utils.d.ts +5 -5
- package/dist/cjs/providers/google/client.d.ts +44 -0
- package/dist/cjs/providers/google/utils.d.ts +2 -3
- package/dist/cjs/providers/openai/client.d.ts +74 -0
- package/dist/cjs/providers/openai/responseFormat.d.ts +17 -0
- package/dist/cjs/providers/openai/utils.d.ts +4 -4
- package/dist/cjs/providers/openrouter/client.d.ts +40 -0
- package/dist/cjs/providers/openrouter/utils.d.ts +2 -3
- package/dist/cjs/providers/xai/utils.d.ts +2 -2
- package/dist/cjs/util/index.d.ts +1 -0
- package/dist/cjs/util/sse.d.ts +12 -0
- package/dist/esm/index.js +734 -112
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/operate/adapters/AnthropicAdapter.d.ts +1 -1
- package/dist/esm/operate/adapters/OpenRouterAdapter.d.ts +8 -1
- package/dist/esm/providers/anthropic/client.d.ts +54 -0
- package/dist/esm/providers/anthropic/types.d.ts +127 -0
- package/dist/esm/providers/anthropic/utils.d.ts +5 -5
- package/dist/esm/providers/google/client.d.ts +44 -0
- package/dist/esm/providers/google/utils.d.ts +2 -3
- package/dist/esm/providers/openai/client.d.ts +74 -0
- package/dist/esm/providers/openai/responseFormat.d.ts +17 -0
- package/dist/esm/providers/openai/utils.d.ts +4 -4
- package/dist/esm/providers/openrouter/client.d.ts +40 -0
- package/dist/esm/providers/openrouter/utils.d.ts +2 -3
- package/dist/esm/providers/xai/utils.d.ts +2 -2
- package/dist/esm/util/index.d.ts +1 -0
- package/dist/esm/util/sse.d.ts +12 -0
- package/package.json +2 -18
package/dist/esm/index.js
CHANGED
|
@@ -3,8 +3,6 @@ import log$2, { log as log$1 } from '@jaypie/logger';
|
|
|
3
3
|
import { z } from 'zod/v4';
|
|
4
4
|
import { placeholders, JAYPIE, resolveValue, sleep } from '@jaypie/kit';
|
|
5
5
|
import RandomLib from 'random';
|
|
6
|
-
import { RateLimitError, APIConnectionError, APIConnectionTimeoutError, InternalServerError, APIUserAbortError, AuthenticationError, BadRequestError, ConflictError, NotFoundError, PermissionDeniedError, UnprocessableEntityError, OpenAI } from 'openai';
|
|
7
|
-
import { zodResponseFormat } from 'openai/helpers/zod';
|
|
8
6
|
import { createRequire } from 'module';
|
|
9
7
|
import { pathToFileURL } from 'url';
|
|
10
8
|
import { PDFDocument } from 'pdf-lib';
|
|
@@ -1085,6 +1083,45 @@ function repairFormatKeys({ content, format, }) {
|
|
|
1085
1083
|
return repairFromSchema(schema, structuredClone(content));
|
|
1086
1084
|
}
|
|
1087
1085
|
|
|
1086
|
+
const DEFAULT_DONE_SENTINEL = "[DONE]";
|
|
1087
|
+
/**
|
|
1088
|
+
* Parse a Server-Sent Events stream into decoded JSON chunks. Buffers across
|
|
1089
|
+
* network reads, dispatches on newline-delimited `data:` lines, and stops at
|
|
1090
|
+
* the done sentinel (OpenAI-style `[DONE]`; Gemini omits it and simply ends
|
|
1091
|
+
* the stream). Comment lines (`: ...`, used for keep-alive) are ignored.
|
|
1092
|
+
*
|
|
1093
|
+
* Shared by the provider HTTP clients that replaced their vendor SDKs.
|
|
1094
|
+
*/
|
|
1095
|
+
async function* parseSseStream(body, { doneSentinel = DEFAULT_DONE_SENTINEL } = {}) {
|
|
1096
|
+
const reader = body.getReader();
|
|
1097
|
+
const decoder = new TextDecoder();
|
|
1098
|
+
let buffer = "";
|
|
1099
|
+
try {
|
|
1100
|
+
for (;;) {
|
|
1101
|
+
const { done, value } = await reader.read();
|
|
1102
|
+
if (done)
|
|
1103
|
+
break;
|
|
1104
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1105
|
+
let newlineIndex;
|
|
1106
|
+
while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
|
|
1107
|
+
const line = buffer.slice(0, newlineIndex).replace(/\r$/, "");
|
|
1108
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
1109
|
+
if (line === "" || line.startsWith(":"))
|
|
1110
|
+
continue;
|
|
1111
|
+
if (!line.startsWith("data:"))
|
|
1112
|
+
continue;
|
|
1113
|
+
const data = line.slice("data:".length).trim();
|
|
1114
|
+
if (data === doneSentinel)
|
|
1115
|
+
return;
|
|
1116
|
+
yield JSON.parse(data);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
finally {
|
|
1121
|
+
reader.releaseLock();
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1088
1125
|
/**
|
|
1089
1126
|
* Helper function to safely call a function that might throw
|
|
1090
1127
|
* @param fn Function to call safely
|
|
@@ -1612,7 +1649,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1612
1649
|
? [...request.tools]
|
|
1613
1650
|
: [];
|
|
1614
1651
|
if (useFallbackStructuredOutput && request.format) {
|
|
1615
|
-
log$
|
|
1652
|
+
log$1.warn(`[AnthropicAdapter] Using legacy structured_output tool fallback for model ${anthropicRequest.model}; native output_config previously rejected for this model.`);
|
|
1616
1653
|
allTools.push({
|
|
1617
1654
|
name: STRUCTURED_OUTPUT_TOOL_NAME$3,
|
|
1618
1655
|
description: "Output a structured JSON object, " +
|
|
@@ -1727,7 +1764,7 @@ class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
1727
1764
|
if (wantsStructuredOutput && isStructuredOutputUnsupportedError$1(error)) {
|
|
1728
1765
|
const model = anthropicRequest.model;
|
|
1729
1766
|
this.rememberModelRejectsStructuredOutput(model);
|
|
1730
|
-
log$
|
|
1767
|
+
log$1.warn(`[AnthropicAdapter] Model ${model} rejected native output_config; falling back to legacy structured_output tool emulation.`);
|
|
1731
1768
|
const fallbackRequest = this.toFallbackStructuredOutputRequest(anthropicRequest);
|
|
1732
1769
|
return (await anthropic.messages.create(fallbackRequest, signal ? { signal } : undefined));
|
|
1733
1770
|
}
|
|
@@ -2790,7 +2827,7 @@ class GoogleAdapter extends BaseProviderAdapter {
|
|
|
2790
2827
|
? [...request.tools]
|
|
2791
2828
|
: [];
|
|
2792
2829
|
if (request.format && hasUserTools && !useNativeCombo) {
|
|
2793
|
-
log$
|
|
2830
|
+
log$1.warn(`[GoogleAdapter] Using legacy structured_output tool fallback for model ${geminiRequest.model}; native responseJsonSchema + tools combo is only available on Gemini 3.`);
|
|
2794
2831
|
allTools.push({
|
|
2795
2832
|
name: STRUCTURED_OUTPUT_TOOL_NAME$1,
|
|
2796
2833
|
description: "Output a structured JSON object, " +
|
|
@@ -2921,7 +2958,7 @@ class GoogleAdapter extends BaseProviderAdapter {
|
|
|
2921
2958
|
if (wantsNativeCombo && isStructuredOutputComboUnsupportedError(error)) {
|
|
2922
2959
|
const model = geminiRequest.model;
|
|
2923
2960
|
this.rememberModelRejectsStructuredOutputCombo(model);
|
|
2924
|
-
log$
|
|
2961
|
+
log$1.warn(`[GoogleAdapter] Model ${model} rejected native responseJsonSchema + tools combo; falling back to legacy structured_output tool emulation.`);
|
|
2925
2962
|
const fallbackRequest = this.toFallbackStructuredOutputRequest(geminiRequest);
|
|
2926
2963
|
const response = await genAI.models.generateContent({
|
|
2927
2964
|
model: fallbackRequest.model,
|
|
@@ -3505,6 +3542,206 @@ class GoogleAdapter extends BaseProviderAdapter {
|
|
|
3505
3542
|
// Export singleton instance
|
|
3506
3543
|
const googleAdapter = new GoogleAdapter();
|
|
3507
3544
|
|
|
3545
|
+
//
|
|
3546
|
+
//
|
|
3547
|
+
// Constants
|
|
3548
|
+
//
|
|
3549
|
+
const OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
3550
|
+
//
|
|
3551
|
+
//
|
|
3552
|
+
// Errors
|
|
3553
|
+
//
|
|
3554
|
+
// The adapter's `classifyError` uses `instanceof` against these classes, so the
|
|
3555
|
+
// client throws them directly. Status → class mapping mirrors the SDK; the
|
|
3556
|
+
// non-HTTP classes (connection/abort) are thrown when `fetch` itself rejects.
|
|
3557
|
+
//
|
|
3558
|
+
class OpenAiApiError extends Error {
|
|
3559
|
+
constructor(status, message, error) {
|
|
3560
|
+
super(message);
|
|
3561
|
+
this.name = this.constructor.name;
|
|
3562
|
+
this.status = status;
|
|
3563
|
+
this.error = error;
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
let BadRequestError$1 = class BadRequestError extends OpenAiApiError {
|
|
3567
|
+
};
|
|
3568
|
+
let AuthenticationError$1 = class AuthenticationError extends OpenAiApiError {
|
|
3569
|
+
};
|
|
3570
|
+
let PermissionDeniedError$1 = class PermissionDeniedError extends OpenAiApiError {
|
|
3571
|
+
};
|
|
3572
|
+
let NotFoundError$1 = class NotFoundError extends OpenAiApiError {
|
|
3573
|
+
};
|
|
3574
|
+
class ConflictError extends OpenAiApiError {
|
|
3575
|
+
}
|
|
3576
|
+
class UnprocessableEntityError extends OpenAiApiError {
|
|
3577
|
+
}
|
|
3578
|
+
let RateLimitError$1 = class RateLimitError extends OpenAiApiError {
|
|
3579
|
+
};
|
|
3580
|
+
let InternalServerError$1 = class InternalServerError extends OpenAiApiError {
|
|
3581
|
+
};
|
|
3582
|
+
class APIConnectionError extends OpenAiApiError {
|
|
3583
|
+
}
|
|
3584
|
+
class APIConnectionTimeoutError extends APIConnectionError {
|
|
3585
|
+
}
|
|
3586
|
+
class APIUserAbortError extends OpenAiApiError {
|
|
3587
|
+
}
|
|
3588
|
+
function errorForStatus$1(status, message, error) {
|
|
3589
|
+
if (status === 400)
|
|
3590
|
+
return new BadRequestError$1(status, message, error);
|
|
3591
|
+
if (status === 401)
|
|
3592
|
+
return new AuthenticationError$1(status, message, error);
|
|
3593
|
+
if (status === 403)
|
|
3594
|
+
return new PermissionDeniedError$1(status, message, error);
|
|
3595
|
+
if (status === 404)
|
|
3596
|
+
return new NotFoundError$1(status, message, error);
|
|
3597
|
+
if (status === 409)
|
|
3598
|
+
return new ConflictError(status, message, error);
|
|
3599
|
+
if (status === 422) {
|
|
3600
|
+
return new UnprocessableEntityError(status, message, error);
|
|
3601
|
+
}
|
|
3602
|
+
if (status === 429)
|
|
3603
|
+
return new RateLimitError$1(status, message, error);
|
|
3604
|
+
if (status >= 500)
|
|
3605
|
+
return new InternalServerError$1(status, message, error);
|
|
3606
|
+
return new OpenAiApiError(status, message, error);
|
|
3607
|
+
}
|
|
3608
|
+
//
|
|
3609
|
+
//
|
|
3610
|
+
// Main
|
|
3611
|
+
//
|
|
3612
|
+
/**
|
|
3613
|
+
* Minimal `fetch`-based client for the OpenAI API. Replaces the `openai` SDK —
|
|
3614
|
+
* the adapters and provider utilities only need the Responses API
|
|
3615
|
+
* (`/responses`, streaming and non-streaming) for `operate`/`stream` and the
|
|
3616
|
+
* Chat Completions API (`/chat/completions`, plus a `parse` helper for
|
|
3617
|
+
* structured output) for `send`. Mirrors the SDK's `responses.create` and
|
|
3618
|
+
* `chat.completions.create` / `chat.completions.parse` surface so call sites are
|
|
3619
|
+
* unchanged. Also serves xAI via a custom `baseURL`.
|
|
3620
|
+
*/
|
|
3621
|
+
class OpenAIClient {
|
|
3622
|
+
constructor({ apiKey, baseURL = OPENAI_BASE_URL }) {
|
|
3623
|
+
this.apiKey = apiKey;
|
|
3624
|
+
this.baseURL = baseURL;
|
|
3625
|
+
this.responses = {
|
|
3626
|
+
create: ((params, options) => params.stream
|
|
3627
|
+
? this.createResponseStream(params, options)
|
|
3628
|
+
: this.createResponse(params, options)),
|
|
3629
|
+
};
|
|
3630
|
+
this.chat = {
|
|
3631
|
+
completions: {
|
|
3632
|
+
create: (params, options) => this.chatCompletion(params, options, { parse: false }),
|
|
3633
|
+
parse: (params, options) => this.chatCompletion(params, options, { parse: true }),
|
|
3634
|
+
},
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3637
|
+
headers(extra) {
|
|
3638
|
+
return {
|
|
3639
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
3640
|
+
"Content-Type": "application/json",
|
|
3641
|
+
...extra,
|
|
3642
|
+
};
|
|
3643
|
+
}
|
|
3644
|
+
async post(path, body, { signal } = {}, { stream = false } = {}) {
|
|
3645
|
+
let response;
|
|
3646
|
+
try {
|
|
3647
|
+
response = await fetch(`${this.baseURL}${path}`, {
|
|
3648
|
+
method: "POST",
|
|
3649
|
+
headers: this.headers(stream ? { Accept: "text/event-stream" } : undefined),
|
|
3650
|
+
body: JSON.stringify(body),
|
|
3651
|
+
signal,
|
|
3652
|
+
});
|
|
3653
|
+
}
|
|
3654
|
+
catch (cause) {
|
|
3655
|
+
if (signal?.aborted) {
|
|
3656
|
+
throw new APIUserAbortError(0, "Request was aborted");
|
|
3657
|
+
}
|
|
3658
|
+
const message = cause instanceof Error ? cause.message : "Connection error";
|
|
3659
|
+
const error = new APIConnectionError(0, message);
|
|
3660
|
+
error.cause = cause;
|
|
3661
|
+
throw error;
|
|
3662
|
+
}
|
|
3663
|
+
if (!response.ok)
|
|
3664
|
+
throw await this.toError(response);
|
|
3665
|
+
return response;
|
|
3666
|
+
}
|
|
3667
|
+
async toError(response) {
|
|
3668
|
+
let message = `OpenAI request failed with status ${response.status}`;
|
|
3669
|
+
let error;
|
|
3670
|
+
try {
|
|
3671
|
+
const body = (await response.json());
|
|
3672
|
+
if (body?.error?.message) {
|
|
3673
|
+
message = body.error.message;
|
|
3674
|
+
error = body.error;
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
catch {
|
|
3678
|
+
// Non-JSON error body; keep the status-based message.
|
|
3679
|
+
}
|
|
3680
|
+
return errorForStatus$1(response.status, message, error);
|
|
3681
|
+
}
|
|
3682
|
+
async createResponse(params, options) {
|
|
3683
|
+
const response = await this.post("/responses", params, options);
|
|
3684
|
+
return (await response.json());
|
|
3685
|
+
}
|
|
3686
|
+
async createResponseStream(params, options) {
|
|
3687
|
+
const response = await this.post("/responses", params, options, {
|
|
3688
|
+
stream: true,
|
|
3689
|
+
});
|
|
3690
|
+
if (!response.body)
|
|
3691
|
+
return (async function* () { })();
|
|
3692
|
+
// The Responses API streams typed events and does not emit a `[DONE]`
|
|
3693
|
+
// sentinel; the empty default sentinel never matches, so the stream ends
|
|
3694
|
+
// when the connection closes.
|
|
3695
|
+
return parseSseStream(response.body);
|
|
3696
|
+
}
|
|
3697
|
+
async chatCompletion(params, options, { parse }) {
|
|
3698
|
+
const response = await this.post("/chat/completions", params, options);
|
|
3699
|
+
const json = (await response.json());
|
|
3700
|
+
// `chat.completions.parse` surfaces parsed JSON on each message; replicate
|
|
3701
|
+
// the SDK by JSON-parsing the assistant content into `message.parsed`.
|
|
3702
|
+
if (parse) {
|
|
3703
|
+
const choices = json.choices;
|
|
3704
|
+
if (Array.isArray(choices)) {
|
|
3705
|
+
for (const choice of choices) {
|
|
3706
|
+
const message = choice.message;
|
|
3707
|
+
if (message &&
|
|
3708
|
+
typeof message.content === "string" &&
|
|
3709
|
+
!message.refusal) {
|
|
3710
|
+
try {
|
|
3711
|
+
message.parsed = JSON.parse(message.content);
|
|
3712
|
+
}
|
|
3713
|
+
catch {
|
|
3714
|
+
message.parsed = null;
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
return json;
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
//
|
|
3725
|
+
//
|
|
3726
|
+
// Main
|
|
3727
|
+
//
|
|
3728
|
+
/**
|
|
3729
|
+
* Local replacement for `openai/helpers/zod`'s `zodResponseFormat`. Builds the
|
|
3730
|
+
* `response_format` payload OpenAI's Chat Completions / Responses APIs expect
|
|
3731
|
+
* from a Zod schema. The SDK always emits `strict: true`; callers re-derive or
|
|
3732
|
+
* sanitize `schema` as needed (e.g. forcing `additionalProperties: false`).
|
|
3733
|
+
*/
|
|
3734
|
+
function zodResponseFormat(schema, name) {
|
|
3735
|
+
return {
|
|
3736
|
+
type: "json_schema",
|
|
3737
|
+
json_schema: {
|
|
3738
|
+
name,
|
|
3739
|
+
strict: true,
|
|
3740
|
+
schema: z.toJSONSchema(schema),
|
|
3741
|
+
},
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3508
3745
|
// Patterns for OpenAI reasoning models that support extended thinking
|
|
3509
3746
|
const REASONING_MODEL_PATTERNS = [
|
|
3510
3747
|
/^gpt-[5-9]/, // GPT-5 and above (gpt-5, gpt-5.1, gpt-5.2, gpt-6, etc.)
|
|
@@ -3523,16 +3760,16 @@ function isReasoningModel(model) {
|
|
|
3523
3760
|
const RETRYABLE_ERROR_TYPES = [
|
|
3524
3761
|
APIConnectionError,
|
|
3525
3762
|
APIConnectionTimeoutError,
|
|
3526
|
-
InternalServerError,
|
|
3763
|
+
InternalServerError$1,
|
|
3527
3764
|
];
|
|
3528
3765
|
const NOT_RETRYABLE_ERROR_TYPES = [
|
|
3529
3766
|
APIUserAbortError,
|
|
3530
|
-
AuthenticationError,
|
|
3531
|
-
BadRequestError,
|
|
3767
|
+
AuthenticationError$1,
|
|
3768
|
+
BadRequestError$1,
|
|
3532
3769
|
ConflictError,
|
|
3533
|
-
NotFoundError,
|
|
3534
|
-
PermissionDeniedError,
|
|
3535
|
-
RateLimitError,
|
|
3770
|
+
NotFoundError$1,
|
|
3771
|
+
PermissionDeniedError$1,
|
|
3772
|
+
RateLimitError$1,
|
|
3536
3773
|
UnprocessableEntityError,
|
|
3537
3774
|
];
|
|
3538
3775
|
// Models known not to accept `temperature`.
|
|
@@ -3546,7 +3783,7 @@ const MODELS_WITHOUT_TEMPERATURE$1 = [
|
|
|
3546
3783
|
function isTemperatureDeprecationError$1(error) {
|
|
3547
3784
|
if (!error || typeof error !== "object")
|
|
3548
3785
|
return false;
|
|
3549
|
-
if (!(error instanceof BadRequestError) &&
|
|
3786
|
+
if (!(error instanceof BadRequestError$1) &&
|
|
3550
3787
|
error.status !== 400) {
|
|
3551
3788
|
return false;
|
|
3552
3789
|
}
|
|
@@ -3900,7 +4137,7 @@ class OpenAiAdapter extends BaseProviderAdapter {
|
|
|
3900
4137
|
//
|
|
3901
4138
|
classifyError(error) {
|
|
3902
4139
|
// Check for rate limit error
|
|
3903
|
-
if (error instanceof RateLimitError) {
|
|
4140
|
+
if (error instanceof RateLimitError$1) {
|
|
3904
4141
|
return {
|
|
3905
4142
|
error,
|
|
3906
4143
|
category: ErrorCategory.RateLimit,
|
|
@@ -4118,7 +4355,49 @@ function convertContentToOpenRouter(content) {
|
|
|
4118
4355
|
}
|
|
4119
4356
|
return parts;
|
|
4120
4357
|
}
|
|
4121
|
-
|
|
4358
|
+
/**
|
|
4359
|
+
* Convert internal content parts to the OpenAI-compatible wire shape. The
|
|
4360
|
+
* internal representation uses camelCase keys (`imageUrl`, `fileData`) that the
|
|
4361
|
+
* SDK accepted; the REST API wants snake_case (`image_url`, `file_data`).
|
|
4362
|
+
*/
|
|
4363
|
+
function contentToWire(content) {
|
|
4364
|
+
if (content === null ||
|
|
4365
|
+
content === undefined ||
|
|
4366
|
+
typeof content === "string") {
|
|
4367
|
+
return content;
|
|
4368
|
+
}
|
|
4369
|
+
return content.map((part) => {
|
|
4370
|
+
if (part.type === "image_url") {
|
|
4371
|
+
return { type: "image_url", image_url: part.imageUrl };
|
|
4372
|
+
}
|
|
4373
|
+
if (part.type === "file") {
|
|
4374
|
+
return {
|
|
4375
|
+
type: "file",
|
|
4376
|
+
file: { filename: part.file.filename, file_data: part.file.fileData },
|
|
4377
|
+
};
|
|
4378
|
+
}
|
|
4379
|
+
return part;
|
|
4380
|
+
});
|
|
4381
|
+
}
|
|
4382
|
+
/**
|
|
4383
|
+
* Serialize an internal message to the OpenAI-compatible wire shape, mapping
|
|
4384
|
+
* camelCase tool fields (`toolCalls`, `toolCallId`) to snake_case. Tool-call
|
|
4385
|
+
* objects are already wire-shaped (`{ id, type, function: { name, arguments } }`).
|
|
4386
|
+
*/
|
|
4387
|
+
function messageToWire(message) {
|
|
4388
|
+
const wire = { role: message.role };
|
|
4389
|
+
if (message.content !== undefined) {
|
|
4390
|
+
wire.content = contentToWire(message.content);
|
|
4391
|
+
}
|
|
4392
|
+
if (message.toolCalls) {
|
|
4393
|
+
wire.tool_calls = message.toolCalls;
|
|
4394
|
+
}
|
|
4395
|
+
if (message.toolCallId) {
|
|
4396
|
+
wire.tool_call_id = message.toolCallId;
|
|
4397
|
+
}
|
|
4398
|
+
return wire;
|
|
4399
|
+
}
|
|
4400
|
+
// OpenRouter error types based on HTTP status codes
|
|
4122
4401
|
const RETRYABLE_STATUS_CODES = [408, 500, 502, 503, 524, 529];
|
|
4123
4402
|
const RATE_LIMIT_STATUS_CODE = 429;
|
|
4124
4403
|
/**
|
|
@@ -4321,7 +4600,7 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
4321
4600
|
const openRouterRequest = request;
|
|
4322
4601
|
const wantsStructuredOutput = Boolean(openRouterRequest.response_format);
|
|
4323
4602
|
try {
|
|
4324
|
-
const response = (await openRouter.
|
|
4603
|
+
const response = (await openRouter.chatCompletion(this.toWireBody(openRouterRequest), signal ? { signal } : undefined));
|
|
4325
4604
|
if (wantsStructuredOutput) {
|
|
4326
4605
|
response.__jaypieStructuredOutput = true;
|
|
4327
4606
|
}
|
|
@@ -4338,7 +4617,7 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
4338
4617
|
this.rememberModelRejectsTemperature(openRouterRequest.model);
|
|
4339
4618
|
const retryRequest = { ...openRouterRequest };
|
|
4340
4619
|
delete retryRequest.temperature;
|
|
4341
|
-
const response = (await openRouter.
|
|
4620
|
+
const response = (await openRouter.chatCompletion(this.toWireBody(retryRequest), signal ? { signal } : undefined));
|
|
4342
4621
|
if (wantsStructuredOutput) {
|
|
4343
4622
|
response.__jaypieStructuredOutput = true;
|
|
4344
4623
|
}
|
|
@@ -4351,7 +4630,7 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
4351
4630
|
this.rememberModelRejectsStructuredOutput(model);
|
|
4352
4631
|
log$1.warn(`[OpenRouterAdapter] Model ${model} rejected native response_format; falling back to legacy structured_output tool emulation.`);
|
|
4353
4632
|
const fallbackRequest = this.toFallbackStructuredOutputRequest(openRouterRequest);
|
|
4354
|
-
return (await openRouter.
|
|
4633
|
+
return (await openRouter.chatCompletion(this.toWireBody(fallbackRequest), signal ? { signal } : undefined));
|
|
4355
4634
|
}
|
|
4356
4635
|
throw error;
|
|
4357
4636
|
}
|
|
@@ -4361,31 +4640,18 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
4361
4640
|
* camelCase shape, forwarding only the fields we care about (the SDK
|
|
4362
4641
|
* silently strips unknown fields).
|
|
4363
4642
|
*/
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4643
|
+
/**
|
|
4644
|
+
* Serialize the internal request into the OpenAI-compatible wire body for
|
|
4645
|
+
* OpenRouter's Chat Completions endpoint. Top-level fields (model, tools,
|
|
4646
|
+
* tool_choice, response_format, user, temperature, and any providerOptions)
|
|
4647
|
+
* are already wire-shaped (snake_case); only messages carry camelCase tool
|
|
4648
|
+
* fields that must become snake_case on the wire.
|
|
4649
|
+
*/
|
|
4650
|
+
toWireBody(openRouterRequest) {
|
|
4651
|
+
return {
|
|
4652
|
+
...openRouterRequest,
|
|
4653
|
+
messages: openRouterRequest.messages.map(messageToWire),
|
|
4371
4654
|
};
|
|
4372
|
-
if (openRouterRequest.response_format) {
|
|
4373
|
-
const format = openRouterRequest.response_format;
|
|
4374
|
-
if (format.type === "json_schema") {
|
|
4375
|
-
params.responseFormat = {
|
|
4376
|
-
type: "json_schema",
|
|
4377
|
-
jsonSchema: format.json_schema,
|
|
4378
|
-
};
|
|
4379
|
-
}
|
|
4380
|
-
else {
|
|
4381
|
-
params.responseFormat = format;
|
|
4382
|
-
}
|
|
4383
|
-
}
|
|
4384
|
-
const temperature = openRouterRequest.temperature;
|
|
4385
|
-
if (temperature !== undefined) {
|
|
4386
|
-
params.temperature = temperature;
|
|
4387
|
-
}
|
|
4388
|
-
return params;
|
|
4389
4655
|
}
|
|
4390
4656
|
/**
|
|
4391
4657
|
* Rebuild a structured-output request without `response_format`, swapping in
|
|
@@ -4417,15 +4683,9 @@ class OpenRouterAdapter extends BaseProviderAdapter {
|
|
|
4417
4683
|
async *executeStreamRequest(client, request, signal) {
|
|
4418
4684
|
const openRouter = client;
|
|
4419
4685
|
const openRouterRequest = request;
|
|
4420
|
-
//
|
|
4421
|
-
//
|
|
4422
|
-
|
|
4423
|
-
// non-stream response.
|
|
4424
|
-
const streamParams = {
|
|
4425
|
-
...this.toSdkChatParams(openRouterRequest),
|
|
4426
|
-
stream: true,
|
|
4427
|
-
};
|
|
4428
|
-
const stream = (await openRouter.chat.send(streamParams, signal ? { signal } : undefined));
|
|
4686
|
+
// streamChatCompletion adds `stream: true` + `stream_options.include_usage`
|
|
4687
|
+
// and yields decoded SSE chunks in OpenAI-compatible (snake_case) shape.
|
|
4688
|
+
const stream = openRouter.streamChatCompletion(this.toWireBody(openRouterRequest), signal ? { signal } : undefined);
|
|
4429
4689
|
// Track current tool call being built
|
|
4430
4690
|
let currentToolCall = null;
|
|
4431
4691
|
// Track usage for final chunk
|
|
@@ -4874,7 +5134,7 @@ const TRANSIENT_INGEST_MESSAGE_PATTERNS = [
|
|
|
4874
5134
|
"failed to ingest inline file bytes",
|
|
4875
5135
|
];
|
|
4876
5136
|
function isTransientIngestError(error) {
|
|
4877
|
-
if (!(error instanceof BadRequestError))
|
|
5137
|
+
if (!(error instanceof BadRequestError$1))
|
|
4878
5138
|
return false;
|
|
4879
5139
|
const message = (error.message ?? "").toLowerCase();
|
|
4880
5140
|
return TRANSIENT_INGEST_MESSAGE_PATTERNS.some((pattern) => message.includes(pattern));
|
|
@@ -5066,7 +5326,7 @@ const requireModule = typeof __filename !== "undefined"
|
|
|
5066
5326
|
? createRequire(pathToFileURL(__filename).href)
|
|
5067
5327
|
: createRequire(import.meta.url);
|
|
5068
5328
|
let resolved = false;
|
|
5069
|
-
let cachedSdk$
|
|
5329
|
+
let cachedSdk$1 = null;
|
|
5070
5330
|
/**
|
|
5071
5331
|
* Lazily resolve the dd-trace `llmobs` SDK. Returns null (and never throws)
|
|
5072
5332
|
* when dd-trace is absent or the SDK surface is unexpected. Cached after the
|
|
@@ -5074,7 +5334,7 @@ let cachedSdk$4 = null;
|
|
|
5074
5334
|
*/
|
|
5075
5335
|
function resolveLlmObs() {
|
|
5076
5336
|
if (resolved) {
|
|
5077
|
-
return cachedSdk$
|
|
5337
|
+
return cachedSdk$1;
|
|
5078
5338
|
}
|
|
5079
5339
|
resolved = true;
|
|
5080
5340
|
try {
|
|
@@ -5084,13 +5344,13 @@ function resolveLlmObs() {
|
|
|
5084
5344
|
if (llmobs &&
|
|
5085
5345
|
typeof llmobs.trace === "function" &&
|
|
5086
5346
|
typeof llmobs.annotate === "function") {
|
|
5087
|
-
cachedSdk$
|
|
5347
|
+
cachedSdk$1 = llmobs;
|
|
5088
5348
|
}
|
|
5089
5349
|
}
|
|
5090
5350
|
catch {
|
|
5091
|
-
cachedSdk$
|
|
5351
|
+
cachedSdk$1 = null;
|
|
5092
5352
|
}
|
|
5093
|
-
return cachedSdk$
|
|
5353
|
+
return cachedSdk$1;
|
|
5094
5354
|
}
|
|
5095
5355
|
//
|
|
5096
5356
|
//
|
|
@@ -7004,19 +7264,125 @@ function createStreamLoop(config) {
|
|
|
7004
7264
|
return new StreamLoop(config);
|
|
7005
7265
|
}
|
|
7006
7266
|
|
|
7007
|
-
//
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7267
|
+
//
|
|
7268
|
+
//
|
|
7269
|
+
// Constants
|
|
7270
|
+
//
|
|
7271
|
+
const ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
|
|
7272
|
+
const ANTHROPIC_VERSION = "2023-06-01";
|
|
7273
|
+
//
|
|
7274
|
+
//
|
|
7275
|
+
// Errors
|
|
7276
|
+
//
|
|
7277
|
+
// The adapter's `classifyError` keys off `error.constructor.name` (the SDK's
|
|
7278
|
+
// class names), so the client throws errors whose class names match. Status →
|
|
7279
|
+
// class mapping mirrors the SDK.
|
|
7280
|
+
//
|
|
7281
|
+
class AnthropicApiError extends Error {
|
|
7282
|
+
constructor(status, message, error) {
|
|
7283
|
+
super(message);
|
|
7284
|
+
this.name = this.constructor.name;
|
|
7285
|
+
this.status = status;
|
|
7286
|
+
this.error = error;
|
|
7015
7287
|
}
|
|
7016
|
-
|
|
7017
|
-
|
|
7288
|
+
}
|
|
7289
|
+
class BadRequestError extends AnthropicApiError {
|
|
7290
|
+
}
|
|
7291
|
+
class AuthenticationError extends AnthropicApiError {
|
|
7292
|
+
}
|
|
7293
|
+
class PermissionDeniedError extends AnthropicApiError {
|
|
7294
|
+
}
|
|
7295
|
+
class NotFoundError extends AnthropicApiError {
|
|
7296
|
+
}
|
|
7297
|
+
class RateLimitError extends AnthropicApiError {
|
|
7298
|
+
}
|
|
7299
|
+
class InternalServerError extends AnthropicApiError {
|
|
7300
|
+
}
|
|
7301
|
+
function errorForStatus(status, message, error) {
|
|
7302
|
+
if (status === 400)
|
|
7303
|
+
return new BadRequestError(status, message, error);
|
|
7304
|
+
if (status === 401)
|
|
7305
|
+
return new AuthenticationError(status, message, error);
|
|
7306
|
+
if (status === 403)
|
|
7307
|
+
return new PermissionDeniedError(status, message, error);
|
|
7308
|
+
if (status === 404)
|
|
7309
|
+
return new NotFoundError(status, message, error);
|
|
7310
|
+
if (status === 429)
|
|
7311
|
+
return new RateLimitError(status, message, error);
|
|
7312
|
+
if (status >= 500)
|
|
7313
|
+
return new InternalServerError(status, message, error);
|
|
7314
|
+
return new AnthropicApiError(status, message, error);
|
|
7315
|
+
}
|
|
7316
|
+
//
|
|
7317
|
+
//
|
|
7318
|
+
// Main
|
|
7319
|
+
//
|
|
7320
|
+
/**
|
|
7321
|
+
* Minimal `fetch`-based client for Anthropic's Messages API. Replaces
|
|
7322
|
+
* `@anthropic-ai/sdk` — the adapter only needs `messages.create` (streaming and
|
|
7323
|
+
* non-streaming), header auth, and HTTP errors whose class names drive
|
|
7324
|
+
* `classifyError`. Internal request params are already Anthropic's wire shape,
|
|
7325
|
+
* so they serialize verbatim. Exposes a `messages` facade mirroring the SDK so
|
|
7326
|
+
* the adapter call sites are unchanged.
|
|
7327
|
+
*/
|
|
7328
|
+
class AnthropicClient {
|
|
7329
|
+
constructor({ apiKey, baseURL = ANTHROPIC_BASE_URL, }) {
|
|
7330
|
+
this.apiKey = apiKey;
|
|
7331
|
+
this.baseURL = baseURL;
|
|
7332
|
+
this.messages = {
|
|
7333
|
+
create: ((params, options) => params.stream
|
|
7334
|
+
? this.createStream(params, options)
|
|
7335
|
+
: this.createMessage(params, options)),
|
|
7336
|
+
};
|
|
7337
|
+
}
|
|
7338
|
+
headers() {
|
|
7339
|
+
return {
|
|
7340
|
+
"anthropic-version": ANTHROPIC_VERSION,
|
|
7341
|
+
"content-type": "application/json",
|
|
7342
|
+
"x-api-key": this.apiKey,
|
|
7343
|
+
};
|
|
7344
|
+
}
|
|
7345
|
+
async toError(response) {
|
|
7346
|
+
let message = `Anthropic request failed with status ${response.status}`;
|
|
7347
|
+
let error;
|
|
7348
|
+
try {
|
|
7349
|
+
const body = (await response.json());
|
|
7350
|
+
if (body?.error?.message) {
|
|
7351
|
+
message = body.error.message;
|
|
7352
|
+
error = body.error;
|
|
7353
|
+
}
|
|
7354
|
+
}
|
|
7355
|
+
catch {
|
|
7356
|
+
// Non-JSON error body; keep the status-based message.
|
|
7357
|
+
}
|
|
7358
|
+
return errorForStatus(response.status, message, error);
|
|
7359
|
+
}
|
|
7360
|
+
async createMessage(params, { signal } = {}) {
|
|
7361
|
+
const response = await fetch(`${this.baseURL}/messages`, {
|
|
7362
|
+
method: "POST",
|
|
7363
|
+
headers: this.headers(),
|
|
7364
|
+
body: JSON.stringify(params),
|
|
7365
|
+
signal,
|
|
7366
|
+
});
|
|
7367
|
+
if (!response.ok)
|
|
7368
|
+
throw await this.toError(response);
|
|
7369
|
+
return (await response.json());
|
|
7370
|
+
}
|
|
7371
|
+
async createStream(params, { signal } = {}) {
|
|
7372
|
+
const response = await fetch(`${this.baseURL}/messages`, {
|
|
7373
|
+
method: "POST",
|
|
7374
|
+
headers: { ...this.headers(), accept: "text/event-stream" },
|
|
7375
|
+
body: JSON.stringify(params),
|
|
7376
|
+
signal,
|
|
7377
|
+
});
|
|
7378
|
+
if (!response.ok)
|
|
7379
|
+
throw await this.toError(response);
|
|
7380
|
+
if (!response.body)
|
|
7381
|
+
return (async function* () { })();
|
|
7382
|
+
return parseSseStream(response.body);
|
|
7018
7383
|
}
|
|
7019
7384
|
}
|
|
7385
|
+
|
|
7020
7386
|
// Logger
|
|
7021
7387
|
const getLogger$5 = () => log$1.lib({ lib: JAYPIE.LIB.LLM });
|
|
7022
7388
|
// Client initialization
|
|
@@ -7026,8 +7392,7 @@ async function initializeClient$5({ apiKey, } = {}) {
|
|
|
7026
7392
|
if (!resolvedApiKey) {
|
|
7027
7393
|
throw new ConfigurationError("The application could not resolve the required API key: ANTHROPIC_API_KEY");
|
|
7028
7394
|
}
|
|
7029
|
-
const
|
|
7030
|
-
const client = new sdk.default({ apiKey: resolvedApiKey });
|
|
7395
|
+
const client = new AnthropicClient({ apiKey: resolvedApiKey });
|
|
7031
7396
|
logger.trace("Initialized Anthropic client");
|
|
7032
7397
|
return client;
|
|
7033
7398
|
}
|
|
@@ -7127,6 +7492,7 @@ async function createStructuredCompletion$1(client, messages, model, responseSch
|
|
|
7127
7492
|
[LlmMessageRole.Assistant]: PROVIDER.ANTHROPIC.ROLE.ASSISTANT,
|
|
7128
7493
|
[LlmMessageRole.Developer]: PROVIDER.ANTHROPIC.ROLE.SYSTEM,
|
|
7129
7494
|
});
|
|
7495
|
+
/* eslint-enable @typescript-eslint/no-namespace */
|
|
7130
7496
|
|
|
7131
7497
|
// Main class implementation
|
|
7132
7498
|
class AnthropicProvider {
|
|
@@ -7215,13 +7581,13 @@ class AnthropicProvider {
|
|
|
7215
7581
|
}
|
|
7216
7582
|
}
|
|
7217
7583
|
|
|
7218
|
-
let cachedSdk
|
|
7219
|
-
async function loadSdk
|
|
7220
|
-
if (cachedSdk
|
|
7221
|
-
return cachedSdk
|
|
7584
|
+
let cachedSdk = null;
|
|
7585
|
+
async function loadSdk() {
|
|
7586
|
+
if (cachedSdk)
|
|
7587
|
+
return cachedSdk;
|
|
7222
7588
|
try {
|
|
7223
|
-
cachedSdk
|
|
7224
|
-
return cachedSdk
|
|
7589
|
+
cachedSdk = await import('@aws-sdk/client-bedrock-runtime');
|
|
7590
|
+
return cachedSdk;
|
|
7225
7591
|
}
|
|
7226
7592
|
catch {
|
|
7227
7593
|
throw new ConfigurationError("@aws-sdk/client-bedrock-runtime is required but not installed. Run: npm install @aws-sdk/client-bedrock-runtime");
|
|
@@ -7231,7 +7597,7 @@ const getLogger$4 = () => log$1.lib({ lib: JAYPIE.LIB.LLM });
|
|
|
7231
7597
|
async function initializeClient$4({ region, } = {}) {
|
|
7232
7598
|
const logger = getLogger$4();
|
|
7233
7599
|
const resolvedRegion = region || process.env.AWS_REGION || "us-east-1";
|
|
7234
|
-
const sdk = await loadSdk
|
|
7600
|
+
const sdk = await loadSdk();
|
|
7235
7601
|
const client = new sdk.BedrockRuntimeClient({ region: resolvedRegion });
|
|
7236
7602
|
logger.trace("Initialized Bedrock client");
|
|
7237
7603
|
return client;
|
|
@@ -7296,19 +7662,156 @@ class BedrockProvider {
|
|
|
7296
7662
|
}
|
|
7297
7663
|
}
|
|
7298
7664
|
|
|
7299
|
-
//
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7665
|
+
//
|
|
7666
|
+
//
|
|
7667
|
+
// Constants
|
|
7668
|
+
//
|
|
7669
|
+
const GOOGLE_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
7670
|
+
// Config keys the REST API expects at the top level of the request body. Every
|
|
7671
|
+
// other key in the SDK-style `config` object belongs under `generationConfig`.
|
|
7672
|
+
const TOP_LEVEL_CONFIG_KEYS = new Set([
|
|
7673
|
+
"systemInstruction",
|
|
7674
|
+
"tools",
|
|
7675
|
+
"toolConfig",
|
|
7676
|
+
"safetySettings",
|
|
7677
|
+
"cachedContent",
|
|
7678
|
+
]);
|
|
7679
|
+
/**
|
|
7680
|
+
* HTTP error carrying the upstream status and parsed API message. The
|
|
7681
|
+
* GoogleAdapter classifies errors by reading `.status` / `.code` and
|
|
7682
|
+
* `.message`, so this shape keeps `classifyError` working unchanged after
|
|
7683
|
+
* dropping the SDK.
|
|
7684
|
+
*/
|
|
7685
|
+
class GoogleHttpError extends Error {
|
|
7686
|
+
constructor(status, message) {
|
|
7687
|
+
super(message);
|
|
7688
|
+
this.name = "GoogleHttpError";
|
|
7689
|
+
this.status = status;
|
|
7690
|
+
this.code = status;
|
|
7307
7691
|
}
|
|
7308
|
-
|
|
7309
|
-
|
|
7692
|
+
}
|
|
7693
|
+
//
|
|
7694
|
+
//
|
|
7695
|
+
// Helpers
|
|
7696
|
+
//
|
|
7697
|
+
/**
|
|
7698
|
+
* Translate the SDK-style `{ model, contents, config }` request into the REST
|
|
7699
|
+
* `generateContent` body. `systemInstruction` (a string) becomes a Content;
|
|
7700
|
+
* top-level config keys pass through; all remaining config keys (temperature,
|
|
7701
|
+
* responseMimeType, responseSchema, responseJsonSchema, ...) move under
|
|
7702
|
+
* `generationConfig`.
|
|
7703
|
+
*/
|
|
7704
|
+
function toRestBody(params) {
|
|
7705
|
+
const body = {
|
|
7706
|
+
contents: params.contents,
|
|
7707
|
+
};
|
|
7708
|
+
const config = params.config;
|
|
7709
|
+
if (!config)
|
|
7710
|
+
return body;
|
|
7711
|
+
const generationConfig = {};
|
|
7712
|
+
for (const [key, value] of Object.entries(config)) {
|
|
7713
|
+
if (value === undefined)
|
|
7714
|
+
continue;
|
|
7715
|
+
if (key === "systemInstruction") {
|
|
7716
|
+
body.systemInstruction =
|
|
7717
|
+
typeof value === "string"
|
|
7718
|
+
? { parts: [{ text: value }] }
|
|
7719
|
+
: value;
|
|
7720
|
+
}
|
|
7721
|
+
else if (TOP_LEVEL_CONFIG_KEYS.has(key)) {
|
|
7722
|
+
body[key] = value;
|
|
7723
|
+
}
|
|
7724
|
+
else {
|
|
7725
|
+
generationConfig[key] = value;
|
|
7726
|
+
}
|
|
7727
|
+
}
|
|
7728
|
+
if (Object.keys(generationConfig).length > 0) {
|
|
7729
|
+
body.generationConfig = generationConfig;
|
|
7310
7730
|
}
|
|
7731
|
+
return body;
|
|
7732
|
+
}
|
|
7733
|
+
/**
|
|
7734
|
+
* Concatenate the non-thought text parts of the first candidate, matching the
|
|
7735
|
+
* SDK's `response.text` convenience getter (consumed by `GoogleProvider.send`).
|
|
7736
|
+
*/
|
|
7737
|
+
function responseText(response) {
|
|
7738
|
+
const parts = response.candidates?.[0]?.content?.parts;
|
|
7739
|
+
if (!parts)
|
|
7740
|
+
return undefined;
|
|
7741
|
+
const text = parts
|
|
7742
|
+
.filter((part) => part.text && !part.thought)
|
|
7743
|
+
.map((part) => part.text)
|
|
7744
|
+
.join("");
|
|
7745
|
+
return text.length > 0 ? text : undefined;
|
|
7311
7746
|
}
|
|
7747
|
+
//
|
|
7748
|
+
//
|
|
7749
|
+
// Main
|
|
7750
|
+
//
|
|
7751
|
+
/**
|
|
7752
|
+
* Minimal `fetch`-based client for Google's Generative Language (Gemini) REST
|
|
7753
|
+
* API. Replaces `@google/genai` — the adapter only needs `generateContent`
|
|
7754
|
+
* (streaming and non-streaming), an api-key header, and HTTP error surfacing.
|
|
7755
|
+
* Exposes a `models` facade mirroring the SDK so the adapter call sites are
|
|
7756
|
+
* unchanged.
|
|
7757
|
+
*/
|
|
7758
|
+
class GoogleClient {
|
|
7759
|
+
constructor({ apiKey, baseURL = GOOGLE_BASE_URL }) {
|
|
7760
|
+
this.apiKey = apiKey;
|
|
7761
|
+
this.baseURL = baseURL;
|
|
7762
|
+
this.models = {
|
|
7763
|
+
generateContent: (params, options) => this.generateContent(params, options),
|
|
7764
|
+
generateContentStream: (params, options) => this.generateContentStream(params, options),
|
|
7765
|
+
};
|
|
7766
|
+
}
|
|
7767
|
+
headers() {
|
|
7768
|
+
return {
|
|
7769
|
+
"Content-Type": "application/json",
|
|
7770
|
+
"x-goog-api-key": this.apiKey,
|
|
7771
|
+
};
|
|
7772
|
+
}
|
|
7773
|
+
async toError(response) {
|
|
7774
|
+
let message = `Google request failed with status ${response.status}`;
|
|
7775
|
+
try {
|
|
7776
|
+
const body = (await response.json());
|
|
7777
|
+
if (body?.error?.message)
|
|
7778
|
+
message = body.error.message;
|
|
7779
|
+
}
|
|
7780
|
+
catch {
|
|
7781
|
+
// Non-JSON error body; keep the status-based message.
|
|
7782
|
+
}
|
|
7783
|
+
return new GoogleHttpError(response.status, message);
|
|
7784
|
+
}
|
|
7785
|
+
async generateContent(params, { signal } = {}) {
|
|
7786
|
+
const url = `${this.baseURL}/models/${params.model}:generateContent`;
|
|
7787
|
+
const response = await fetch(url, {
|
|
7788
|
+
method: "POST",
|
|
7789
|
+
headers: this.headers(),
|
|
7790
|
+
body: JSON.stringify(toRestBody(params)),
|
|
7791
|
+
signal,
|
|
7792
|
+
});
|
|
7793
|
+
if (!response.ok)
|
|
7794
|
+
throw await this.toError(response);
|
|
7795
|
+
const json = (await response.json());
|
|
7796
|
+
json.text = responseText(json);
|
|
7797
|
+
return json;
|
|
7798
|
+
}
|
|
7799
|
+
async generateContentStream(params, { signal } = {}) {
|
|
7800
|
+
const url = `${this.baseURL}/models/${params.model}:streamGenerateContent?alt=sse`;
|
|
7801
|
+
const response = await fetch(url, {
|
|
7802
|
+
method: "POST",
|
|
7803
|
+
headers: { ...this.headers(), Accept: "text/event-stream" },
|
|
7804
|
+
body: JSON.stringify(toRestBody(params)),
|
|
7805
|
+
signal,
|
|
7806
|
+
});
|
|
7807
|
+
if (!response.ok)
|
|
7808
|
+
throw await this.toError(response);
|
|
7809
|
+
if (!response.body)
|
|
7810
|
+
return (async function* () { })();
|
|
7811
|
+
return parseSseStream(response.body);
|
|
7812
|
+
}
|
|
7813
|
+
}
|
|
7814
|
+
|
|
7312
7815
|
// Logger
|
|
7313
7816
|
const getLogger$3 = () => log$1.lib({ lib: JAYPIE.LIB.LLM });
|
|
7314
7817
|
// Client initialization
|
|
@@ -7318,8 +7821,7 @@ async function initializeClient$3({ apiKey, } = {}) {
|
|
|
7318
7821
|
if (!resolvedApiKey) {
|
|
7319
7822
|
throw new ConfigurationError("The application could not resolve the requested keys");
|
|
7320
7823
|
}
|
|
7321
|
-
const
|
|
7322
|
-
const client = new sdk.GoogleGenAI({ apiKey: resolvedApiKey });
|
|
7824
|
+
const client = new GoogleClient({ apiKey: resolvedApiKey });
|
|
7323
7825
|
logger.trace("Initialized Gemini client");
|
|
7324
7826
|
return client;
|
|
7325
7827
|
}
|
|
@@ -7468,7 +7970,7 @@ async function initializeClient$2({ apiKey, } = {}) {
|
|
|
7468
7970
|
if (!resolvedApiKey) {
|
|
7469
7971
|
throw new ConfigurationError("The application could not resolve the requested keys");
|
|
7470
7972
|
}
|
|
7471
|
-
const client = new
|
|
7973
|
+
const client = new OpenAIClient({ apiKey: resolvedApiKey });
|
|
7472
7974
|
logger.trace("Initialized OpenAI client");
|
|
7473
7975
|
return client;
|
|
7474
7976
|
}
|
|
@@ -7527,23 +8029,23 @@ async function createStructuredCompletion(client, { messages, responseSchema, mo
|
|
|
7527
8029
|
checks.shift();
|
|
7528
8030
|
}
|
|
7529
8031
|
responseFormat.json_schema.schema = jsonSchema;
|
|
7530
|
-
const completion = await client.chat.completions.parse({
|
|
8032
|
+
const completion = (await client.chat.completions.parse({
|
|
7531
8033
|
messages,
|
|
7532
8034
|
model,
|
|
7533
8035
|
response_format: responseFormat,
|
|
7534
|
-
});
|
|
8036
|
+
}));
|
|
7535
8037
|
logger.var({ assistantReply: completion.choices[0].message.parsed });
|
|
7536
8038
|
return completion.choices[0].message.parsed;
|
|
7537
8039
|
}
|
|
7538
8040
|
async function createTextCompletion(client, { messages, model, }) {
|
|
7539
8041
|
const logger = getLogger$2();
|
|
7540
8042
|
logger.trace("Using text output (unstructured)");
|
|
7541
|
-
const completion = await client.chat.completions.create({
|
|
8043
|
+
const completion = (await client.chat.completions.create({
|
|
7542
8044
|
messages,
|
|
7543
8045
|
model,
|
|
7544
|
-
});
|
|
7545
|
-
logger.trace(`Assistant reply: ${completion.choices[0]?.message?.content?.length} characters`);
|
|
7546
|
-
return completion.choices[0]?.message?.content || "";
|
|
8046
|
+
}));
|
|
8047
|
+
logger.trace(`Assistant reply: ${completion.choices?.[0]?.message?.content?.length} characters`);
|
|
8048
|
+
return completion.choices?.[0]?.message?.content || "";
|
|
7547
8049
|
}
|
|
7548
8050
|
|
|
7549
8051
|
class OpenAiProvider {
|
|
@@ -7630,19 +8132,139 @@ class OpenAiProvider {
|
|
|
7630
8132
|
}
|
|
7631
8133
|
}
|
|
7632
8134
|
|
|
7633
|
-
//
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
8135
|
+
//
|
|
8136
|
+
//
|
|
8137
|
+
// Constants
|
|
8138
|
+
//
|
|
8139
|
+
const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
8140
|
+
/**
|
|
8141
|
+
* HTTP error carrying the upstream status and parsed API message. The
|
|
8142
|
+
* OpenRouterAdapter classifies errors by reading `.status` / `.statusCode` and
|
|
8143
|
+
* `.message` / `.error.message`, so this shape keeps `classifyError`,
|
|
8144
|
+
* `isTemperatureDeprecationError`, and `isStructuredOutputUnsupportedError`
|
|
8145
|
+
* working unchanged after dropping the SDK.
|
|
8146
|
+
*/
|
|
8147
|
+
class OpenRouterHttpError extends Error {
|
|
8148
|
+
constructor(status, message, error) {
|
|
8149
|
+
super(message);
|
|
8150
|
+
this.name = "OpenRouterHttpError";
|
|
8151
|
+
this.status = status;
|
|
8152
|
+
this.statusCode = status;
|
|
8153
|
+
this.error = error;
|
|
7641
8154
|
}
|
|
7642
|
-
|
|
7643
|
-
|
|
8155
|
+
}
|
|
8156
|
+
//
|
|
8157
|
+
//
|
|
8158
|
+
// Helpers
|
|
8159
|
+
//
|
|
8160
|
+
/**
|
|
8161
|
+
* Normalize the snake_case wire response into the camelCase shape the adapter
|
|
8162
|
+
* readers expect (the SDK previously returned camelCase). Only protocol fields
|
|
8163
|
+
* are touched — user content (schema property names, tool argument JSON) is
|
|
8164
|
+
* left untouched.
|
|
8165
|
+
*/
|
|
8166
|
+
function normalizeResponse(json) {
|
|
8167
|
+
const choices = json.choices;
|
|
8168
|
+
if (Array.isArray(choices)) {
|
|
8169
|
+
for (const choice of choices) {
|
|
8170
|
+
if (choice.finish_reason !== undefined &&
|
|
8171
|
+
choice.finishReason === undefined) {
|
|
8172
|
+
choice.finishReason = choice.finish_reason;
|
|
8173
|
+
}
|
|
8174
|
+
const message = choice.message;
|
|
8175
|
+
if (message?.tool_calls !== undefined &&
|
|
8176
|
+
message.toolCalls === undefined) {
|
|
8177
|
+
message.toolCalls = message.tool_calls;
|
|
8178
|
+
}
|
|
8179
|
+
}
|
|
8180
|
+
}
|
|
8181
|
+
const usage = json.usage;
|
|
8182
|
+
if (usage) {
|
|
8183
|
+
if (usage.promptTokens === undefined)
|
|
8184
|
+
usage.promptTokens = usage.prompt_tokens;
|
|
8185
|
+
if (usage.completionTokens === undefined) {
|
|
8186
|
+
usage.completionTokens = usage.completion_tokens;
|
|
8187
|
+
}
|
|
8188
|
+
if (usage.totalTokens === undefined)
|
|
8189
|
+
usage.totalTokens = usage.total_tokens;
|
|
8190
|
+
const details = usage.completion_tokens_details;
|
|
8191
|
+
if (details?.reasoning_tokens !== undefined &&
|
|
8192
|
+
!usage.completionTokensDetails) {
|
|
8193
|
+
usage.completionTokensDetails = {
|
|
8194
|
+
reasoningTokens: details.reasoning_tokens,
|
|
8195
|
+
};
|
|
8196
|
+
}
|
|
7644
8197
|
}
|
|
8198
|
+
return json;
|
|
7645
8199
|
}
|
|
8200
|
+
//
|
|
8201
|
+
//
|
|
8202
|
+
// Main
|
|
8203
|
+
//
|
|
8204
|
+
/**
|
|
8205
|
+
* Minimal `fetch`-based client for OpenRouter's OpenAI-compatible Chat
|
|
8206
|
+
* Completions endpoint. Replaces `@openrouter/sdk` — the adapter only needs a
|
|
8207
|
+
* single POST (streaming and non-streaming), header auth, and HTTP error
|
|
8208
|
+
* surfacing.
|
|
8209
|
+
*/
|
|
8210
|
+
class OpenRouterClient {
|
|
8211
|
+
constructor({ apiKey, baseURL = OPENROUTER_BASE_URL, }) {
|
|
8212
|
+
this.apiKey = apiKey;
|
|
8213
|
+
this.baseURL = baseURL;
|
|
8214
|
+
}
|
|
8215
|
+
headers() {
|
|
8216
|
+
return {
|
|
8217
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
8218
|
+
"Content-Type": "application/json",
|
|
8219
|
+
};
|
|
8220
|
+
}
|
|
8221
|
+
async toError(response) {
|
|
8222
|
+
let message = `OpenRouter request failed with status ${response.status}`;
|
|
8223
|
+
let error;
|
|
8224
|
+
try {
|
|
8225
|
+
const body = (await response.json());
|
|
8226
|
+
if (body?.error?.message) {
|
|
8227
|
+
message = body.error.message;
|
|
8228
|
+
error = body.error;
|
|
8229
|
+
}
|
|
8230
|
+
}
|
|
8231
|
+
catch {
|
|
8232
|
+
// Non-JSON error body; keep the status-based message.
|
|
8233
|
+
}
|
|
8234
|
+
return new OpenRouterHttpError(response.status, message, error);
|
|
8235
|
+
}
|
|
8236
|
+
async chatCompletion(body, { signal } = {}) {
|
|
8237
|
+
const response = await fetch(`${this.baseURL}/chat/completions`, {
|
|
8238
|
+
method: "POST",
|
|
8239
|
+
headers: this.headers(),
|
|
8240
|
+
body: JSON.stringify(body),
|
|
8241
|
+
signal,
|
|
8242
|
+
});
|
|
8243
|
+
if (!response.ok)
|
|
8244
|
+
throw await this.toError(response);
|
|
8245
|
+
const json = (await response.json());
|
|
8246
|
+
return normalizeResponse(json);
|
|
8247
|
+
}
|
|
8248
|
+
async *streamChatCompletion(body, { signal } = {}) {
|
|
8249
|
+
const response = await fetch(`${this.baseURL}/chat/completions`, {
|
|
8250
|
+
method: "POST",
|
|
8251
|
+
headers: { ...this.headers(), Accept: "text/event-stream" },
|
|
8252
|
+
// OpenAI-style streams only include usage when explicitly requested.
|
|
8253
|
+
body: JSON.stringify({
|
|
8254
|
+
...body,
|
|
8255
|
+
stream: true,
|
|
8256
|
+
stream_options: { include_usage: true },
|
|
8257
|
+
}),
|
|
8258
|
+
signal,
|
|
8259
|
+
});
|
|
8260
|
+
if (!response.ok)
|
|
8261
|
+
throw await this.toError(response);
|
|
8262
|
+
if (!response.body)
|
|
8263
|
+
return;
|
|
8264
|
+
yield* parseSseStream(response.body);
|
|
8265
|
+
}
|
|
8266
|
+
}
|
|
8267
|
+
|
|
7646
8268
|
// Logger
|
|
7647
8269
|
const getLogger$1 = () => log$1.lib({ lib: JAYPIE.LIB.LLM });
|
|
7648
8270
|
// Client initialization
|
|
@@ -7652,8 +8274,7 @@ async function initializeClient$1({ apiKey, } = {}) {
|
|
|
7652
8274
|
if (!resolvedApiKey) {
|
|
7653
8275
|
throw new ConfigurationError("The application could not resolve the requested keys");
|
|
7654
8276
|
}
|
|
7655
|
-
const
|
|
7656
|
-
const client = new sdk.OpenRouter({ apiKey: resolvedApiKey });
|
|
8277
|
+
const client = new OpenRouterClient({ apiKey: resolvedApiKey });
|
|
7657
8278
|
logger.trace("Initialized OpenRouter client");
|
|
7658
8279
|
return client;
|
|
7659
8280
|
}
|
|
@@ -7733,12 +8354,13 @@ class OpenRouterProvider {
|
|
|
7733
8354
|
const client = await this.getClient();
|
|
7734
8355
|
const messages = prepareMessages(message, options);
|
|
7735
8356
|
const modelToUse = options?.model || this.model;
|
|
7736
|
-
//
|
|
7737
|
-
const response = await client.
|
|
8357
|
+
// OpenAI-compatible Chat Completions body; messages are already wire-shaped.
|
|
8358
|
+
const response = await client.chatCompletion({
|
|
7738
8359
|
model: modelToUse,
|
|
7739
|
-
messages
|
|
8360
|
+
messages,
|
|
7740
8361
|
});
|
|
7741
|
-
const
|
|
8362
|
+
const choices = response.choices;
|
|
8363
|
+
const rawContent = choices?.[0]?.message?.content;
|
|
7742
8364
|
// Extract text content - content could be string or array of content items
|
|
7743
8365
|
const content = typeof rawContent === "string"
|
|
7744
8366
|
? rawContent
|
|
@@ -7801,7 +8423,7 @@ async function initializeClient({ apiKey, } = {}) {
|
|
|
7801
8423
|
if (!resolvedApiKey) {
|
|
7802
8424
|
throw new ConfigurationError("The application could not resolve the requested keys");
|
|
7803
8425
|
}
|
|
7804
|
-
const client = new
|
|
8426
|
+
const client = new OpenAIClient({
|
|
7805
8427
|
apiKey: resolvedApiKey,
|
|
7806
8428
|
baseURL: PROVIDER.XAI.BASE_URL,
|
|
7807
8429
|
});
|