@byfriends/kosong 0.1.0

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.
@@ -0,0 +1,278 @@
1
+ import { a as APITimeoutError, o as ChatProviderError, s as normalizeAPIStatusError, t as APIConnectionError$1 } from "./errors-WFxxzL1B.mjs";
2
+ import { APIConnectionError, APIConnectionTimeoutError, APIError, OpenAIError } from "openai";
3
+ //#region src/message.ts
4
+ /** Check if a streamed part is a ContentPart (text, think, image_url, audio_url, video_url). */
5
+ function isContentPart(part) {
6
+ const t = part.type;
7
+ return t === "text" || t === "think" || t === "image_url" || t === "audio_url" || t === "video_url";
8
+ }
9
+ /** Check if a streamed part is a ToolCall. */
10
+ function isToolCall(part) {
11
+ return part.type === "function";
12
+ }
13
+ /** Check if a streamed part is a ToolCallPart (streaming argument delta). */
14
+ function isToolCallPart(part) {
15
+ return part.type === "tool_call_part";
16
+ }
17
+ /**
18
+ * Merge `source` into `target` in-place for streaming accumulation.
19
+ *
20
+ * Supported combinations:
21
+ * - TextPart + TextPart -> concatenate text
22
+ * - ThinkPart + ThinkPart -> concatenate think (refuse if target.encrypted already set)
23
+ * - ToolCall + ToolCallPart -> append arguments
24
+ *
25
+ * **Routing for parallel tool calls**: When OpenAI (or compatible) APIs stream
26
+ * multiple tool calls in parallel, argument deltas may interleave across calls.
27
+ * To handle this, {@link generate} routes ToolCallParts by their optional
28
+ * {@link ToolCallPart.index} field (mirroring the provider's streaming index)
29
+ * to the correct pending ToolCall, rather than relying on sequential ordering.
30
+ * This function still performs sequential merging as a fallback when the
31
+ * pending part matches the incoming one.
32
+ *
33
+ * Returns `true` if the merge was performed, `false` otherwise.
34
+ */
35
+ function mergeInPlace(target, source) {
36
+ if (target.type === "text" && source.type === "text") {
37
+ target.text += source.text;
38
+ return true;
39
+ }
40
+ if (target.type === "think" && source.type === "think") {
41
+ if (target.encrypted !== void 0) return false;
42
+ target.think += source.think;
43
+ if (source.encrypted !== void 0) target.encrypted = source.encrypted;
44
+ return true;
45
+ }
46
+ if (target.type === "function" && source.type === "tool_call_part") {
47
+ if (source.argumentsPart !== null) target.arguments = target.arguments === null ? source.argumentsPart : target.arguments + source.argumentsPart;
48
+ return true;
49
+ }
50
+ return false;
51
+ }
52
+ /**
53
+ * Extract the concatenated text from a message's content parts.
54
+ *
55
+ * @param message The message to extract text from.
56
+ * @param sep Separator between text parts. Defaults to empty string.
57
+ */
58
+ function extractText(message, sep = "") {
59
+ return message.content.filter((part) => part.type === "text").map((part) => part.text).join(sep);
60
+ }
61
+ /** Create a simple user message with a single text part. */
62
+ function createUserMessage(content) {
63
+ return {
64
+ role: "user",
65
+ content: [{
66
+ type: "text",
67
+ text: content
68
+ }],
69
+ toolCalls: []
70
+ };
71
+ }
72
+ /** Create an assistant message from content parts and optional tool calls. */
73
+ function createAssistantMessage(content, toolCalls) {
74
+ return {
75
+ role: "assistant",
76
+ content,
77
+ toolCalls: toolCalls ?? []
78
+ };
79
+ }
80
+ /** Create a tool result message. */
81
+ function createToolMessage(toolCallId, output) {
82
+ return {
83
+ role: "tool",
84
+ content: typeof output === "string" ? [{
85
+ type: "text",
86
+ text: output
87
+ }] : output,
88
+ toolCalls: [],
89
+ toolCallId
90
+ };
91
+ }
92
+ //#endregion
93
+ //#region src/providers/openai-common.ts
94
+ /**
95
+ * Convert a kosong `ContentPart` to OpenAI-compatible content part.
96
+ * Returns `null` for think parts (handled separately as reasoning_content).
97
+ */
98
+ function convertContentPart(part) {
99
+ switch (part.type) {
100
+ case "text": return {
101
+ type: "text",
102
+ text: part.text
103
+ };
104
+ case "think": return null;
105
+ case "image_url": return {
106
+ type: "image_url",
107
+ image_url: part.imageUrl.id === void 0 ? { url: part.imageUrl.url } : {
108
+ url: part.imageUrl.url,
109
+ id: part.imageUrl.id
110
+ }
111
+ };
112
+ case "audio_url": return {
113
+ type: "audio_url",
114
+ audio_url: part.audioUrl.id === void 0 ? { url: part.audioUrl.url } : {
115
+ url: part.audioUrl.url,
116
+ id: part.audioUrl.id
117
+ }
118
+ };
119
+ case "video_url": return {
120
+ type: "video_url",
121
+ video_url: part.videoUrl.id === void 0 ? { url: part.videoUrl.url } : {
122
+ url: part.videoUrl.url,
123
+ id: part.videoUrl.id
124
+ }
125
+ };
126
+ default: throw new Error(`Unknown content part type: ${part.type}`);
127
+ }
128
+ }
129
+ /**
130
+ * Convert a kosong `Tool` to OpenAI tool format.
131
+ */
132
+ function toolToOpenAI(tool) {
133
+ return {
134
+ type: "function",
135
+ function: {
136
+ name: tool.name,
137
+ description: tool.description,
138
+ parameters: tool.parameters
139
+ }
140
+ };
141
+ }
142
+ const NETWORK_RE = /network|connection|connect|disconnect/i;
143
+ const TIMEOUT_RE = /timed?\s*out|timeout|deadline/i;
144
+ function classifyBaseApiError(message) {
145
+ if (TIMEOUT_RE.test(message)) return new APITimeoutError(message);
146
+ if (NETWORK_RE.test(message)) return new APIConnectionError$1(message);
147
+ return new ChatProviderError(`Error: ${message}`);
148
+ }
149
+ /**
150
+ * Convert an OpenAI SDK error (or raw Error) to a kosong `ChatProviderError`.
151
+ */
152
+ function convertOpenAIError(error) {
153
+ if (error instanceof ChatProviderError) return error;
154
+ if (error instanceof APIConnectionTimeoutError) return new APITimeoutError(error.message);
155
+ if (error instanceof APIConnectionError) return new APIConnectionError$1(error.message);
156
+ if (error instanceof APIError && typeof error.status === "number") {
157
+ const reqId = error.requestID ?? null;
158
+ return normalizeAPIStatusError(error.status, error.message, reqId);
159
+ }
160
+ if (error instanceof APIError && error.constructor === APIError && error.error === void 0) return classifyBaseApiError(error.message);
161
+ if (error instanceof OpenAIError) return new ChatProviderError(`Error: ${error.message}`);
162
+ if (error instanceof Error) return new ChatProviderError(`Error: ${error.message}`);
163
+ return new ChatProviderError(`Error: ${String(error)}`);
164
+ }
165
+ /**
166
+ * Type guard: narrow a tool call union to the function-type variant.
167
+ * Works with OpenAI SDK's `ChatCompletionMessageToolCall` as well as
168
+ * any object carrying `{ type: string }`.
169
+ */
170
+ function isFunctionToolCall(tc) {
171
+ return tc.type === "function";
172
+ }
173
+ /**
174
+ * Map kosong `ThinkingEffort` to OpenAI `reasoning_effort` string.
175
+ */
176
+ function thinkingEffortToReasoningEffort(effort) {
177
+ switch (effort) {
178
+ case "off": return;
179
+ case "low": return "low";
180
+ case "medium": return "medium";
181
+ case "high": return "high";
182
+ case "xhigh":
183
+ case "max": return "xhigh";
184
+ default: throw new Error(`Unknown thinking effort: ${String(effort)}`);
185
+ }
186
+ }
187
+ /**
188
+ * Map OpenAI `reasoning_effort` string back to kosong `ThinkingEffort`.
189
+ */
190
+ function reasoningEffortToThinkingEffort(reasoning) {
191
+ if (reasoning === void 0 || reasoning === null) return null;
192
+ switch (reasoning) {
193
+ case "low":
194
+ case "minimal": return "low";
195
+ case "medium": return "medium";
196
+ case "high": return "high";
197
+ case "xhigh":
198
+ case "max": return "xhigh";
199
+ case "none": return "off";
200
+ default: return "off";
201
+ }
202
+ }
203
+ /**
204
+ * Extract `TokenUsage` from an OpenAI-compatible usage object.
205
+ */
206
+ function extractUsage(usage) {
207
+ if (usage === null || usage === void 0 || typeof usage !== "object") return null;
208
+ const u = usage;
209
+ const promptTokens = typeof u["prompt_tokens"] === "number" ? u["prompt_tokens"] : 0;
210
+ const completionTokens = typeof u["completion_tokens"] === "number" ? u["completion_tokens"] : 0;
211
+ let cached = 0;
212
+ if (typeof u["cached_tokens"] === "number") cached = u["cached_tokens"];
213
+ else if (typeof u["prompt_tokens_details"] === "object" && u["prompt_tokens_details"] !== null) {
214
+ const details = u["prompt_tokens_details"];
215
+ if (typeof details["cached_tokens"] === "number") cached = details["cached_tokens"];
216
+ }
217
+ return {
218
+ inputOther: promptTokens - cached,
219
+ output: completionTokens,
220
+ inputCacheRead: cached,
221
+ inputCacheCreation: 0
222
+ };
223
+ }
224
+ /**
225
+ * Normalize an OpenAI Chat Completions–style `finish_reason` string to the
226
+ * unified {@link FinishReason} enum.
227
+ *
228
+ * Used by both the Byf and OpenAI Legacy adapters because they share the
229
+ * Chat Completions wire format. Returns `{ finishReason: null,
230
+ * rawFinishReason: null }` when the upstream value is missing or `null` so
231
+ * callers can treat "no signal" uniformly.
232
+ *
233
+ * Mapping:
234
+ * - `'stop'` → `'completed'`
235
+ * - `'tool_calls'` → `'tool_calls'`
236
+ * - `'function_call'` → `'tool_calls'` (legacy alias)
237
+ * - `'length'` → `'truncated'`
238
+ * - `'content_filter'` → `'filtered'`
239
+ * - any other non-null string → `'other'`
240
+ */
241
+ function normalizeOpenAIFinishReason(raw) {
242
+ if (raw === null || raw === void 0) return {
243
+ finishReason: null,
244
+ rawFinishReason: null
245
+ };
246
+ switch (raw) {
247
+ case "stop": return {
248
+ finishReason: "completed",
249
+ rawFinishReason: raw
250
+ };
251
+ case "tool_calls":
252
+ case "function_call": return {
253
+ finishReason: "tool_calls",
254
+ rawFinishReason: raw
255
+ };
256
+ case "length": return {
257
+ finishReason: "truncated",
258
+ rawFinishReason: raw
259
+ };
260
+ case "content_filter": return {
261
+ finishReason: "filtered",
262
+ rawFinishReason: raw
263
+ };
264
+ default: return {
265
+ finishReason: "other",
266
+ rawFinishReason: raw
267
+ };
268
+ }
269
+ }
270
+ /**
271
+ * Convert tool-role message content according to the chosen strategy.
272
+ */
273
+ function convertToolMessageContent(message, conversion) {
274
+ if (conversion === "extract_text") return extractText(message);
275
+ return message.content.map((p) => convertContentPart(p)).filter((p) => p !== null);
276
+ }
277
+ //#endregion
278
+ export { mergeInPlace as _, isFunctionToolCall as a, thinkingEffortToReasoningEffort as c, createToolMessage as d, createUserMessage as f, isToolCallPart as g, isToolCall as h, extractUsage as i, toolToOpenAI as l, isContentPart as m, convertOpenAIError as n, normalizeOpenAIFinishReason as o, extractText as p, convertToolMessageContent as r, reasoningEffortToThinkingEffort as s, convertContentPart as t, createAssistantMessage as u };
@@ -0,0 +1,105 @@
1
+ import { b as Message, c as TokenUsage, n as FinishReason, o as ThinkingEffort, p as Tool, v as ContentPart } from "./provider-DiJKWMsQ.mjs";
2
+ import { o as ChatProviderError } from "./errors-DweKbIOf.mjs";
3
+
4
+ //#region src/providers/openai-common.d.ts
5
+ interface OpenAIContentPart {
6
+ type: string;
7
+ text?: string | undefined;
8
+ image_url?: {
9
+ url: string;
10
+ id?: string | null;
11
+ } | undefined;
12
+ audio_url?: {
13
+ url: string;
14
+ id?: string | null;
15
+ } | undefined;
16
+ video_url?: {
17
+ url: string;
18
+ id?: string | null;
19
+ } | undefined;
20
+ }
21
+ /**
22
+ * Convert a kosong `ContentPart` to OpenAI-compatible content part.
23
+ * Returns `null` for think parts (handled separately as reasoning_content).
24
+ */
25
+ declare function convertContentPart(part: ContentPart): OpenAIContentPart | null;
26
+ interface OpenAIToolParam {
27
+ type: string;
28
+ function: {
29
+ name: string;
30
+ description?: string;
31
+ parameters?: Record<string, unknown>;
32
+ };
33
+ }
34
+ /**
35
+ * Convert a kosong `Tool` to OpenAI tool format.
36
+ */
37
+ declare function toolToOpenAI(tool: Tool): OpenAIToolParam;
38
+ /**
39
+ * Convert an OpenAI SDK error (or raw Error) to a kosong `ChatProviderError`.
40
+ */
41
+ declare function convertOpenAIError(error: unknown): ChatProviderError;
42
+ /** Shape of a function-type tool call (subset used by the guard). */
43
+ interface FunctionToolCallShape {
44
+ type: 'function';
45
+ id: string;
46
+ function: {
47
+ name: string;
48
+ arguments: string | null;
49
+ };
50
+ }
51
+ /**
52
+ * Type guard: narrow a tool call union to the function-type variant.
53
+ * Works with OpenAI SDK's `ChatCompletionMessageToolCall` as well as
54
+ * any object carrying `{ type: string }`.
55
+ */
56
+ declare function isFunctionToolCall<T extends {
57
+ type: string;
58
+ }>(tc: T): tc is T & FunctionToolCallShape;
59
+ /**
60
+ * Map kosong `ThinkingEffort` to OpenAI `reasoning_effort` string.
61
+ */
62
+ declare function thinkingEffortToReasoningEffort(effort: ThinkingEffort): string | undefined;
63
+ /**
64
+ * Map OpenAI `reasoning_effort` string back to kosong `ThinkingEffort`.
65
+ */
66
+ declare function reasoningEffortToThinkingEffort(reasoning: string | undefined): ThinkingEffort | null;
67
+ /**
68
+ * Extract `TokenUsage` from an OpenAI-compatible usage object.
69
+ */
70
+ declare function extractUsage(usage: unknown): TokenUsage | null;
71
+ /**
72
+ * Normalize an OpenAI Chat Completions–style `finish_reason` string to the
73
+ * unified {@link FinishReason} enum.
74
+ *
75
+ * Used by both the Byf and OpenAI Legacy adapters because they share the
76
+ * Chat Completions wire format. Returns `{ finishReason: null,
77
+ * rawFinishReason: null }` when the upstream value is missing or `null` so
78
+ * callers can treat "no signal" uniformly.
79
+ *
80
+ * Mapping:
81
+ * - `'stop'` → `'completed'`
82
+ * - `'tool_calls'` → `'tool_calls'`
83
+ * - `'function_call'` → `'tool_calls'` (legacy alias)
84
+ * - `'length'` → `'truncated'`
85
+ * - `'content_filter'` → `'filtered'`
86
+ * - any other non-null string → `'other'`
87
+ */
88
+ declare function normalizeOpenAIFinishReason(raw: string | null | undefined): {
89
+ finishReason: FinishReason | null;
90
+ rawFinishReason: string | null;
91
+ };
92
+ /**
93
+ * Strategy for converting tool-role message content.
94
+ *
95
+ * - `'extract_text'`: flatten all content parts into a single text string
96
+ * (some providers require tool results as plain text).
97
+ * - `null`: convert content parts to the standard OpenAI content-part array.
98
+ */
99
+ type ToolMessageConversion = 'extract_text' | null;
100
+ /**
101
+ * Convert tool-role message content according to the chosen strategy.
102
+ */
103
+ declare function convertToolMessageContent(message: Message, conversion: ToolMessageConversion): string | OpenAIContentPart[];
104
+ //#endregion
105
+ export { convertContentPart as a, extractUsage as c, reasoningEffortToThinkingEffort as d, thinkingEffortToReasoningEffort as f, ToolMessageConversion as i, isFunctionToolCall as l, OpenAIContentPart as n, convertOpenAIError as o, toolToOpenAI as p, OpenAIToolParam as r, convertToolMessageContent as s, FunctionToolCallShape as t, normalizeOpenAIFinishReason as u };
@@ -0,0 +1,132 @@
1
+ import { D as VideoURLPart, a as StreamedMessage, b as Message, i as ProviderRequestAuth, m as ModelCapability, o as ThinkingEffort, p as Tool, r as GenerateOptions, s as VideoUploadInput, t as ChatProvider } from "./provider-DiJKWMsQ.mjs";
2
+ import OpenAI from "openai";
3
+
4
+ //#region src/providers/openai-compat-files.d.ts
5
+ interface OpenAICompatUploadOptions {
6
+ auth?: ProviderRequestAuth;
7
+ signal?: AbortSignal;
8
+ }
9
+ interface OpenAICompatFilesOptions {
10
+ apiKey?: string;
11
+ baseUrl: string;
12
+ defaultHeaders?: Record<string, string>;
13
+ clientFactory?: (auth: ProviderRequestAuth) => OpenAI;
14
+ }
15
+ /**
16
+ * OpenAI-compatible file upload client.
17
+ *
18
+ * Wraps the underlying OpenAI-compatible `files.create` API to upload videos
19
+ * to the file service and return them as {@link VideoURLPart} values
20
+ * suitable for use in chat messages.
21
+ *
22
+ * An `OpenAICompatFiles` instance is typically obtained from
23
+ * {@link OpenAICompatChatProvider.files}.
24
+ */
25
+ declare class OpenAICompatFiles {
26
+ private readonly _apiKey;
27
+ private readonly _baseUrl;
28
+ private readonly _defaultHeaders;
29
+ private readonly _client;
30
+ private readonly _clientFactory;
31
+ constructor(options: OpenAICompatFilesOptions);
32
+ /**
33
+ * Upload a video file for use in chat messages.
34
+ *
35
+ * Accepts either a local filesystem path or an in-memory
36
+ * {@link VideoUploadInput}. Returns a {@link VideoURLPart} referencing the
37
+ * uploaded file by its file id.
38
+ *
39
+ * @param input - Local path string or `{ data, mimeType }` object.
40
+ * @returns A `VideoURLPart` whose `url` references the uploaded file
41
+ * by its file id (e.g. `ms://<file-id>`).
42
+ * @throws {ChatProviderError} if the input is not a video or the upload
43
+ * fails.
44
+ */
45
+ uploadVideo(input: string | VideoUploadInput, options?: OpenAICompatUploadOptions): Promise<VideoURLPart>;
46
+ private _createClient;
47
+ }
48
+ //#endregion
49
+ //#region src/providers/openai-compat.d.ts
50
+ interface OpenAICompatOptions {
51
+ apiKey?: string;
52
+ baseUrl?: string;
53
+ model: string;
54
+ stream?: boolean;
55
+ defaultHeaders?: Record<string, string>;
56
+ thinkingEffortKey?: string;
57
+ generationKwargs?: GenerationKwargs;
58
+ clientFactory?: (auth: ProviderRequestAuth) => OpenAI;
59
+ }
60
+ interface GenerationKwargs {
61
+ /**
62
+ * Legacy completion-budget alias. Some OpenAI-compatible APIs still accept
63
+ * `max_tokens`, but for reasoning models it shares the budget with
64
+ * `reasoning_content` and a small value can cause a 200 response with no
65
+ * `content`. Prefer `max_completion_tokens`. When both are set
66
+ * `max_completion_tokens` wins; this provider normalizes by sending only
67
+ * `max_completion_tokens` on the wire.
68
+ */
69
+ max_tokens?: number | undefined;
70
+ max_completion_tokens?: number | undefined;
71
+ temperature?: number | undefined;
72
+ top_p?: number | undefined;
73
+ n?: number | undefined;
74
+ presence_penalty?: number | undefined;
75
+ frequency_penalty?: number | undefined;
76
+ stop?: string | string[] | undefined;
77
+ reasoning_effort?: string | undefined;
78
+ prompt_cache_key?: string | undefined;
79
+ extra_body?: ExtraBody;
80
+ [key: string]: unknown;
81
+ }
82
+ interface ThinkingConfig {
83
+ type?: 'enabled' | 'disabled';
84
+ keep?: unknown;
85
+ [key: string]: unknown;
86
+ }
87
+ interface ExtraBody {
88
+ thinking?: ThinkingConfig;
89
+ [key: string]: unknown;
90
+ }
91
+ /**
92
+ * Extract usage from a streaming chunk. Some OpenAI-compatible providers may place usage in
93
+ * `choices[0].usage` in addition to the top-level `usage` field.
94
+ */
95
+ declare function extractUsageFromChunk(chunk: Record<string, unknown>): Record<string, unknown> | null;
96
+ declare class OpenAICompatChatProvider implements ChatProvider {
97
+ readonly name: string;
98
+ private _model;
99
+ private _stream;
100
+ private _apiKey;
101
+ private _baseUrl;
102
+ private _defaultHeaders;
103
+ private _generationKwargs;
104
+ private _thinkingEffortKey;
105
+ private _client;
106
+ private _clientFactory;
107
+ private _files;
108
+ constructor(options: OpenAICompatOptions);
109
+ get modelName(): string;
110
+ /**
111
+ * File upload client for an OpenAI-compatible service.
112
+ *
113
+ * Use this to upload videos (and other media in the future) to the file
114
+ * service and receive a content part that can be embedded in chat
115
+ * messages.
116
+ */
117
+ get files(): OpenAICompatFiles;
118
+ uploadVideo(input: string | VideoUploadInput, options?: GenerateOptions): Promise<VideoURLPart>;
119
+ get thinkingEffort(): ThinkingEffort | null;
120
+ get modelParameters(): Record<string, unknown>;
121
+ generate(systemPrompt: string, tools: Tool[], history: Message[], options?: GenerateOptions): Promise<StreamedMessage>;
122
+ getCapability(_model?: string): ModelCapability;
123
+ withThinking(effort: ThinkingEffort): OpenAICompatChatProvider;
124
+ withGenerationKwargs(kwargs: GenerationKwargs): OpenAICompatChatProvider;
125
+ withMaxCompletionTokens(maxCompletionTokens: number): OpenAICompatChatProvider;
126
+ withExtraBody(extraBody: ExtraBody): OpenAICompatChatProvider;
127
+ private _createClient;
128
+ private _withGenerationKwargs;
129
+ private _clone;
130
+ }
131
+ //#endregion
132
+ export { ThinkingConfig as a, OpenAICompatOptions as i, GenerationKwargs as n, extractUsageFromChunk as o, OpenAICompatChatProvider as r, ExtraBody as t };