@robota-sdk/agent-provider 3.0.0-beta.64
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/LICENSE +21 -0
- package/dist/browser/index.d.ts +1104 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +7 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/loggers/index.cjs +1 -0
- package/dist/loggers/index.d.ts +151 -0
- package/dist/loggers/index.d.ts.map +1 -0
- package/dist/loggers/index.js +2 -0
- package/dist/loggers/index.js.map +1 -0
- package/dist/node/anthropic/index.cjs +1 -0
- package/dist/node/anthropic/index.d.ts +158 -0
- package/dist/node/anthropic/index.d.ts.map +1 -0
- package/dist/node/anthropic/index.js +1 -0
- package/dist/node/anthropic--1vgLC-e.js +5 -0
- package/dist/node/anthropic--1vgLC-e.js.map +1 -0
- package/dist/node/anthropic-BFQ6DSCP.cjs +4 -0
- package/dist/node/bytedance/index.cjs +1 -0
- package/dist/node/bytedance/index.d.ts +74 -0
- package/dist/node/bytedance/index.d.ts.map +1 -0
- package/dist/node/bytedance/index.js +1 -0
- package/dist/node/bytedance-C_0sF_pJ.js +2 -0
- package/dist/node/bytedance-C_0sF_pJ.js.map +1 -0
- package/dist/node/bytedance-DVPxqEiC.cjs +1 -0
- package/dist/node/chunk-Bmb41Sf3.cjs +1 -0
- package/dist/node/deepseek/index.cjs +1 -0
- package/dist/node/deepseek/index.d.ts +2 -0
- package/dist/node/deepseek/index.js +1 -0
- package/dist/node/deepseek-_8Ixx7rA.js +2 -0
- package/dist/node/deepseek-_8Ixx7rA.js.map +1 -0
- package/dist/node/deepseek-oA2Y6bD0.cjs +1 -0
- package/dist/node/gemini/index.cjs +1 -0
- package/dist/node/gemini/index.d.ts +173 -0
- package/dist/node/gemini/index.d.ts.map +1 -0
- package/dist/node/gemini/index.js +1 -0
- package/dist/node/gemini-Bh2U87MY.js +4 -0
- package/dist/node/gemini-Bh2U87MY.js.map +1 -0
- package/dist/node/gemini-DSaNCxZj.cjs +3 -0
- package/dist/node/gemma/index.cjs +1 -0
- package/dist/node/gemma/index.d.ts +2 -0
- package/dist/node/gemma/index.js +1 -0
- package/dist/node/gemma-Dp_AfCUR.js +2 -0
- package/dist/node/gemma-Dp_AfCUR.js.map +1 -0
- package/dist/node/gemma-G-Pf_PnX.cjs +1 -0
- package/dist/node/google/index.cjs +1 -0
- package/dist/node/google/index.d.ts +14 -0
- package/dist/node/google/index.d.ts.map +1 -0
- package/dist/node/google/index.js +2 -0
- package/dist/node/google/index.js.map +1 -0
- package/dist/node/index-B6PnlDMd.d.ts +82 -0
- package/dist/node/index-B6PnlDMd.d.ts.map +1 -0
- package/dist/node/index-B7UvPJcI.d.ts +315 -0
- package/dist/node/index-B7UvPJcI.d.ts.map +1 -0
- package/dist/node/index-BLPOTNb5.d.ts +98 -0
- package/dist/node/index-BLPOTNb5.d.ts.map +1 -0
- package/dist/node/index-BqixM_XD.d.ts +231 -0
- package/dist/node/index-BqixM_XD.d.ts.map +1 -0
- package/dist/node/index-C3beaqKO.d.ts +231 -0
- package/dist/node/index-C3beaqKO.d.ts.map +1 -0
- package/dist/node/index-Cp2XRh9G.d.ts +82 -0
- package/dist/node/index-Cp2XRh9G.d.ts.map +1 -0
- package/dist/node/index-DSv5xruI.d.ts +98 -0
- package/dist/node/index-DSv5xruI.d.ts.map +1 -0
- package/dist/node/index-w0bV1uaP.d.ts +315 -0
- package/dist/node/index-w0bV1uaP.d.ts.map +1 -0
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.d.ts +8 -0
- package/dist/node/index.js +1 -0
- package/dist/node/openai/index.cjs +1 -0
- package/dist/node/openai/index.d.ts +2 -0
- package/dist/node/openai/index.js +1 -0
- package/dist/node/openai-CRQjg4xF.js +2 -0
- package/dist/node/openai-CRQjg4xF.js.map +1 -0
- package/dist/node/openai-compatible-BYfyY5lb.cjs +1 -0
- package/dist/node/openai-compatible-Dm4Sof9e.js +2 -0
- package/dist/node/openai-compatible-Dm4Sof9e.js.map +1 -0
- package/dist/node/openai-xWC6pY7r.cjs +1 -0
- package/dist/node/qwen/index.cjs +1 -0
- package/dist/node/qwen/index.d.ts +2 -0
- package/dist/node/qwen/index.js +1 -0
- package/dist/node/qwen-ChUZobTL.js +2 -0
- package/dist/node/qwen-ChUZobTL.js.map +1 -0
- package/dist/node/qwen-CjT71vSM.cjs +1 -0
- package/package.json +157 -0
- package/src/anthropic/__tests__/abort-streaming.test.ts +199 -0
- package/src/anthropic/__tests__/model-catalog-refresh.test.ts +92 -0
- package/src/anthropic/__tests__/provider-definition.test.ts +55 -0
- package/src/anthropic/__tests__/provider.test.ts +1357 -0
- package/src/anthropic/__tests__/response-parser.test.ts +326 -0
- package/src/anthropic/index.ts +22 -0
- package/src/anthropic/message-converter.ts +181 -0
- package/src/anthropic/model-catalog-refresh.ts +128 -0
- package/src/anthropic/parsers/response-parser.ts +184 -0
- package/src/anthropic/provider-definition.ts +93 -0
- package/src/anthropic/provider.ts +290 -0
- package/src/anthropic/streaming-handler.ts +204 -0
- package/src/anthropic/types/api-types.ts +158 -0
- package/src/anthropic/types.ts +79 -0
- package/src/bytedance/http-client.test.ts +288 -0
- package/src/bytedance/http-client.ts +163 -0
- package/src/bytedance/index.ts +2 -0
- package/src/bytedance/provider.spec.ts +320 -0
- package/src/bytedance/provider.ts +171 -0
- package/src/bytedance/status-mapper.test.ts +299 -0
- package/src/bytedance/status-mapper.ts +141 -0
- package/src/bytedance/types.ts +68 -0
- package/src/deepseek/defaults.ts +4 -0
- package/src/deepseek/index.ts +22 -0
- package/src/deepseek/model-catalog-refresh.test.ts +57 -0
- package/src/deepseek/model-catalog-refresh.ts +105 -0
- package/src/deepseek/model-catalog.ts +55 -0
- package/src/deepseek/provider-definition.test.ts +109 -0
- package/src/deepseek/provider-definition.ts +132 -0
- package/src/deepseek/provider.test.ts +324 -0
- package/src/deepseek/provider.ts +298 -0
- package/src/deepseek/types.ts +37 -0
- package/src/gemini/execution-helpers.ts +233 -0
- package/src/gemini/genai-transport.test.ts +208 -0
- package/src/gemini/image-operations.test.ts +448 -0
- package/src/gemini/image-operations.ts +261 -0
- package/src/gemini/index.ts +11 -0
- package/src/gemini/message-converter.test.ts +616 -0
- package/src/gemini/message-converter.ts +140 -0
- package/src/gemini/model-catalog-refresh.test.ts +107 -0
- package/src/gemini/model-catalog-refresh.ts +92 -0
- package/src/gemini/provider-definition.test.ts +70 -0
- package/src/gemini/provider-definition.ts +78 -0
- package/src/gemini/provider-extended.test.ts +898 -0
- package/src/gemini/provider.spec.ts +216 -0
- package/src/gemini/provider.ts +279 -0
- package/src/gemini/request-converter.ts +226 -0
- package/src/gemini/tool-schema-converter.ts +78 -0
- package/src/gemini/types/api-types.ts +235 -0
- package/src/gemini/types.ts +121 -0
- package/src/gemma/index.ts +5 -0
- package/src/gemma/message-factory.ts +38 -0
- package/src/gemma/provider-definition.test.ts +43 -0
- package/src/gemma/provider-definition.ts +84 -0
- package/src/gemma/provider-projection.ts +49 -0
- package/src/gemma/provider.test.ts +628 -0
- package/src/gemma/provider.ts +308 -0
- package/src/gemma/pseudo-command-envelope.ts +58 -0
- package/src/gemma/pseudo-tool-call-projector.ts +243 -0
- package/src/gemma/pseudo-tool-call-tag-parser.ts +153 -0
- package/src/gemma/pseudo-tool-call-types.ts +31 -0
- package/src/gemma/reasoning-projector.test.ts +52 -0
- package/src/gemma/reasoning-projector.ts +144 -0
- package/src/gemma/streaming-projection.ts +79 -0
- package/src/gemma/tool-call-argument-parser.ts +126 -0
- package/src/gemma/tool-call-projector.test.ts +227 -0
- package/src/gemma/tool-call-projector.ts +264 -0
- package/src/gemma/types.ts +27 -0
- package/src/google/index.ts +11 -0
- package/src/google/provider-compat.test.ts +19 -0
- package/src/google/provider-definition.ts +6 -0
- package/src/google/provider.ts +10 -0
- package/src/google/types.ts +5 -0
- package/src/index.ts +9 -0
- package/src/openai/adapter.test.ts +494 -0
- package/src/openai/adapter.ts +145 -0
- package/src/openai/chat-completions-chat.ts +189 -0
- package/src/openai/executor-integration.test.ts +206 -0
- package/src/openai/index.ts +21 -0
- package/src/openai/interfaces/payload-logger.ts +48 -0
- package/src/openai/loggers/console-payload-logger.test.ts +173 -0
- package/src/openai/loggers/console-payload-logger.ts +94 -0
- package/src/openai/loggers/console.ts +9 -0
- package/src/openai/loggers/file-payload-logger.test.ts +238 -0
- package/src/openai/loggers/file-payload-logger.ts +112 -0
- package/src/openai/loggers/file.ts +9 -0
- package/src/openai/loggers/index.ts +12 -0
- package/src/openai/loggers/sanitize-openai-log-data.test.ts +89 -0
- package/src/openai/loggers/sanitize-openai-log-data.ts +14 -0
- package/src/openai/message-converter.ts +22 -0
- package/src/openai/model-catalog-refresh.test.ts +92 -0
- package/src/openai/model-catalog-refresh.ts +115 -0
- package/src/openai/openai-request-format.ts +92 -0
- package/src/openai/parsers/response-parser.test.ts +407 -0
- package/src/openai/parsers/response-parser.ts +47 -0
- package/src/openai/provider-definition.test.ts +75 -0
- package/src/openai/provider-definition.ts +132 -0
- package/src/openai/provider.test.ts +1402 -0
- package/src/openai/provider.ts +237 -0
- package/src/openai/responses-chat.ts +258 -0
- package/src/openai/responses-converter.ts +112 -0
- package/src/openai/responses-parser.ts +285 -0
- package/src/openai/responses-stream-utils.ts +45 -0
- package/src/openai/responses-types.ts +195 -0
- package/src/openai/streaming/stream-assembler.ts +3 -0
- package/src/openai/streaming/stream-handler.test.ts +367 -0
- package/src/openai/streaming/stream-handler.ts +119 -0
- package/src/openai/types/api-types.ts +112 -0
- package/src/openai/types.ts +194 -0
- package/src/qwen/defaults.ts +26 -0
- package/src/qwen/index.ts +5 -0
- package/src/qwen/model-catalog-refresh.test.ts +91 -0
- package/src/qwen/model-catalog-refresh.ts +97 -0
- package/src/qwen/provider-capabilities.ts +34 -0
- package/src/qwen/provider-definition.test.ts +139 -0
- package/src/qwen/provider-definition.ts +173 -0
- package/src/qwen/provider-streaming-assembly.ts +40 -0
- package/src/qwen/provider.test.ts +640 -0
- package/src/qwen/provider.ts +293 -0
- package/src/qwen/responses-chat.ts +194 -0
- package/src/qwen/responses-converter.ts +104 -0
- package/src/qwen/responses-parser.ts +299 -0
- package/src/qwen/responses-stream-utils.ts +38 -0
- package/src/qwen/types.ts +228 -0
- package/src/shared/openai-compatible/endpoint-probe.test.ts +52 -0
- package/src/shared/openai-compatible/endpoint-probe.ts +43 -0
- package/src/shared/openai-compatible/index.ts +6 -0
- package/src/shared/openai-compatible/message-converter.test.ts +111 -0
- package/src/shared/openai-compatible/message-converter.ts +84 -0
- package/src/shared/openai-compatible/native-payload-observer.test.ts +43 -0
- package/src/shared/openai-compatible/native-payload-observer.ts +26 -0
- package/src/shared/openai-compatible/response-parser.test.ts +172 -0
- package/src/shared/openai-compatible/response-parser.ts +180 -0
- package/src/shared/openai-compatible/stream-assembler.test.ts +266 -0
- package/src/shared/openai-compatible/stream-assembler.ts +248 -0
- package/src/shared/openai-compatible/types.ts +59 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type {
|
|
3
|
+
IOpenAIModelCatalogFetchInit,
|
|
4
|
+
IOpenAIModelCatalogFetchResponse,
|
|
5
|
+
} from './model-catalog-refresh';
|
|
6
|
+
import { refreshOpenAIModelCatalog } from './model-catalog-refresh';
|
|
7
|
+
|
|
8
|
+
function createModelCatalogResponse(
|
|
9
|
+
body: IOpenAIModelCatalogFetchResponse['json'],
|
|
10
|
+
): IOpenAIModelCatalogFetchResponse {
|
|
11
|
+
return {
|
|
12
|
+
ok: true,
|
|
13
|
+
status: 200,
|
|
14
|
+
json: body,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('refreshOpenAIModelCatalog', () => {
|
|
19
|
+
it('queries the OpenAI models endpoint and returns live catalog entries', async () => {
|
|
20
|
+
const requests: Array<{ url: string; init: IOpenAIModelCatalogFetchInit }> = [];
|
|
21
|
+
const catalog = await refreshOpenAIModelCatalog(
|
|
22
|
+
{ type: 'openai', model: 'gpt-5.1', apiKey: 'sk-test' },
|
|
23
|
+
{
|
|
24
|
+
now: () => new Date('2026-05-05T00:00:00.000Z'),
|
|
25
|
+
fetcher: async (url, init) => {
|
|
26
|
+
requests.push({ url, init });
|
|
27
|
+
return createModelCatalogResponse(async () => ({
|
|
28
|
+
data: [{ id: 'gpt-5.1' }, { id: 'gpt-5.1-mini' }, { id: '' }],
|
|
29
|
+
}));
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(requests).toEqual([
|
|
35
|
+
{
|
|
36
|
+
url: 'https://api.openai.com/v1/models',
|
|
37
|
+
init: { headers: { Authorization: 'Bearer sk-test' } },
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
40
|
+
expect(catalog).toMatchObject({
|
|
41
|
+
status: 'live',
|
|
42
|
+
lastVerifiedAt: '2026-05-05T00:00:00.000Z',
|
|
43
|
+
sourceUrl: 'https://platform.openai.com/docs/api-reference/models/list',
|
|
44
|
+
});
|
|
45
|
+
expect(catalog.entries?.map((entry) => entry.id)).toEqual(['gpt-5.1', 'gpt-5.1-mini']);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('uses profile baseURL for OpenAI-compatible model discovery', async () => {
|
|
49
|
+
const requestedUrls: string[] = [];
|
|
50
|
+
|
|
51
|
+
await refreshOpenAIModelCatalog(
|
|
52
|
+
{
|
|
53
|
+
type: 'openai',
|
|
54
|
+
model: 'local-model',
|
|
55
|
+
apiKey: 'local-key',
|
|
56
|
+
baseURL: 'http://localhost:1234/v1/',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
fetcher: async (url) => {
|
|
60
|
+
requestedUrls.push(url);
|
|
61
|
+
return createModelCatalogResponse(async () => ({ data: [] }));
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(requestedUrls).toEqual(['http://localhost:1234/v1/models']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('returns unavailable catalog state when credentials are missing', async () => {
|
|
70
|
+
const catalog = await refreshOpenAIModelCatalog({ type: 'openai', model: 'gpt-5.1' });
|
|
71
|
+
|
|
72
|
+
expect(catalog.status).toBe('unavailable');
|
|
73
|
+
expect(catalog.message).toBe('OpenAI model catalog refresh requires apiKey.');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns unavailable catalog state when the endpoint rejects discovery', async () => {
|
|
77
|
+
const catalog = await refreshOpenAIModelCatalog(
|
|
78
|
+
{ type: 'openai', model: 'gpt-5.1', apiKey: 'sk-test' },
|
|
79
|
+
{
|
|
80
|
+
fetcher: async () => ({
|
|
81
|
+
ok: false,
|
|
82
|
+
status: 401,
|
|
83
|
+
statusText: 'Unauthorized',
|
|
84
|
+
json: async () => ({ data: [] }),
|
|
85
|
+
}),
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(catalog.status).toBe('unavailable');
|
|
90
|
+
expect(catalog.message).toBe('OpenAI model catalog refresh failed: HTTP 401 Unauthorized');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IProviderModelCatalog,
|
|
3
|
+
IProviderModelCatalogEntry,
|
|
4
|
+
IProviderProfileConfig,
|
|
5
|
+
} from '@robota-sdk/agent-core';
|
|
6
|
+
|
|
7
|
+
const OPENAI_MODELS_ENDPOINT = 'https://api.openai.com/v1/models';
|
|
8
|
+
const OPENAI_MODELS_SOURCE_URL = 'https://platform.openai.com/docs/api-reference/models/list';
|
|
9
|
+
|
|
10
|
+
export interface IOpenAIModelCatalogResponse {
|
|
11
|
+
data?: readonly IOpenAIModelCatalogResource[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface IOpenAIModelCatalogResource {
|
|
15
|
+
id?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface IOpenAIModelCatalogFetchInit {
|
|
19
|
+
headers: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IOpenAIModelCatalogFetchResponse {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
status: number;
|
|
25
|
+
statusText?: string;
|
|
26
|
+
json: () => Promise<IOpenAIModelCatalogResponse>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type TOpenAIModelCatalogFetch = (
|
|
30
|
+
url: string,
|
|
31
|
+
init: IOpenAIModelCatalogFetchInit,
|
|
32
|
+
) => Promise<IOpenAIModelCatalogFetchResponse>;
|
|
33
|
+
|
|
34
|
+
export interface IRefreshOpenAIModelCatalogOptions {
|
|
35
|
+
fetcher?: TOpenAIModelCatalogFetch;
|
|
36
|
+
now?: () => Date;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function refreshOpenAIModelCatalog(
|
|
40
|
+
profile: IProviderProfileConfig,
|
|
41
|
+
options: IRefreshOpenAIModelCatalogOptions = {},
|
|
42
|
+
): Promise<IProviderModelCatalog> {
|
|
43
|
+
if (!profile.apiKey) {
|
|
44
|
+
return createUnavailableCatalog('OpenAI model catalog refresh requires apiKey.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const fetcher = options.fetcher ?? defaultOpenAIModelCatalogFetch;
|
|
48
|
+
const now = options.now ?? (() => new Date());
|
|
49
|
+
const url = resolveModelsEndpoint(profile.baseURL);
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetcher(url, {
|
|
53
|
+
headers: { Authorization: `Bearer ${profile.apiKey}` },
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
return createUnavailableCatalog(formatHttpFailure(response));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const body = await response.json();
|
|
60
|
+
const entries = toModelCatalogEntries(body);
|
|
61
|
+
return {
|
|
62
|
+
status: 'live',
|
|
63
|
+
entries,
|
|
64
|
+
lastVerifiedAt: now().toISOString(),
|
|
65
|
+
sourceUrl: OPENAI_MODELS_SOURCE_URL,
|
|
66
|
+
message: `${entries.length} OpenAI model(s) discovered.`,
|
|
67
|
+
};
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return createUnavailableCatalog(error instanceof Error ? error.message : String(error));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveModelsEndpoint(baseURL: string | undefined): string {
|
|
74
|
+
if (!baseURL) return OPENAI_MODELS_ENDPOINT;
|
|
75
|
+
return `${baseURL.replace(/\/$/, '')}/models`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function toModelCatalogEntries(
|
|
79
|
+
body: IOpenAIModelCatalogResponse,
|
|
80
|
+
): readonly IProviderModelCatalogEntry[] {
|
|
81
|
+
return (body.data ?? [])
|
|
82
|
+
.map((model) => model.id)
|
|
83
|
+
.filter((id): id is string => id !== undefined && id.trim().length > 0)
|
|
84
|
+
.map((id) => ({
|
|
85
|
+
id,
|
|
86
|
+
displayName: id,
|
|
87
|
+
lifecycle: 'active',
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function formatHttpFailure(response: IOpenAIModelCatalogFetchResponse): string {
|
|
92
|
+
const statusText = response.statusText ? ` ${response.statusText}` : '';
|
|
93
|
+
return `OpenAI model catalog refresh failed: HTTP ${response.status}${statusText}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function createUnavailableCatalog(message: string): IProviderModelCatalog {
|
|
97
|
+
return {
|
|
98
|
+
status: 'unavailable',
|
|
99
|
+
sourceUrl: OPENAI_MODELS_SOURCE_URL,
|
|
100
|
+
message,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function defaultOpenAIModelCatalogFetch(
|
|
105
|
+
url: string,
|
|
106
|
+
init: IOpenAIModelCatalogFetchInit,
|
|
107
|
+
): Promise<IOpenAIModelCatalogFetchResponse> {
|
|
108
|
+
const response = await fetch(url, init);
|
|
109
|
+
return {
|
|
110
|
+
ok: response.ok,
|
|
111
|
+
status: response.status,
|
|
112
|
+
statusText: response.statusText,
|
|
113
|
+
json: () => response.json() as Promise<IOpenAIModelCatalogResponse>,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { IOpenAIProviderOptions, TOpenAIProviderOptionValue } from './types';
|
|
2
|
+
import type {
|
|
3
|
+
IOpenAIResponsesTextConfig,
|
|
4
|
+
IOpenAIResponsesTextFormatJsonSchema,
|
|
5
|
+
} from './responses-types';
|
|
6
|
+
|
|
7
|
+
export interface IOpenAIChatTextFormatText {
|
|
8
|
+
type: 'text';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface IOpenAIChatTextFormatJsonObject {
|
|
12
|
+
type: 'json_object';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface IOpenAIChatTextFormatJsonSchema {
|
|
16
|
+
type: 'json_schema';
|
|
17
|
+
json_schema: {
|
|
18
|
+
name: string;
|
|
19
|
+
schema: Record<string, TOpenAIProviderOptionValue>;
|
|
20
|
+
description?: string;
|
|
21
|
+
strict?: boolean;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type TOpenAIChatResponseFormat =
|
|
26
|
+
| IOpenAIChatTextFormatText
|
|
27
|
+
| IOpenAIChatTextFormatJsonObject
|
|
28
|
+
| IOpenAIChatTextFormatJsonSchema;
|
|
29
|
+
|
|
30
|
+
export function buildOpenAIChatResponseFormat(
|
|
31
|
+
options: IOpenAIProviderOptions,
|
|
32
|
+
): TOpenAIChatResponseFormat | undefined {
|
|
33
|
+
if (options.responseFormat === undefined || options.responseFormat === 'text') {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
if (options.responseFormat === 'json_object') {
|
|
37
|
+
return { type: 'json_object' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const schema = requireJsonSchema(options);
|
|
41
|
+
return {
|
|
42
|
+
type: 'json_schema',
|
|
43
|
+
json_schema: {
|
|
44
|
+
name: schema.name,
|
|
45
|
+
schema: schema.schema,
|
|
46
|
+
...(schema.description !== undefined && { description: schema.description }),
|
|
47
|
+
...(schema.strict !== undefined && { strict: schema.strict }),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildOpenAIResponsesTextConfig(
|
|
53
|
+
options: IOpenAIProviderOptions,
|
|
54
|
+
): IOpenAIResponsesTextConfig | undefined {
|
|
55
|
+
if (options.responseFormat === undefined || options.responseFormat === 'text') {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
if (options.responseFormat === 'json_object') {
|
|
59
|
+
return { format: { type: 'json_object' } };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const schema = requireJsonSchema(options);
|
|
63
|
+
const format: IOpenAIResponsesTextFormatJsonSchema = {
|
|
64
|
+
type: 'json_schema',
|
|
65
|
+
name: schema.name,
|
|
66
|
+
schema: schema.schema,
|
|
67
|
+
...(schema.description !== undefined && { description: schema.description }),
|
|
68
|
+
...(schema.strict !== undefined && { strict: schema.strict }),
|
|
69
|
+
};
|
|
70
|
+
return { format };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function requireJsonSchema(input: IOpenAIProviderOptions): {
|
|
74
|
+
name: string;
|
|
75
|
+
schema: Record<string, TOpenAIProviderOptionValue>;
|
|
76
|
+
description?: string;
|
|
77
|
+
strict?: boolean;
|
|
78
|
+
} {
|
|
79
|
+
const schema = input.jsonSchema;
|
|
80
|
+
if (input.responseFormat !== 'json_schema') {
|
|
81
|
+
throw new Error(`Unsupported OpenAI response format: ${input.responseFormat}`);
|
|
82
|
+
}
|
|
83
|
+
if (!schema?.schema) {
|
|
84
|
+
throw new Error('OpenAI jsonSchema.schema is required when responseFormat is json_schema');
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
name: schema.name,
|
|
88
|
+
schema: schema.schema,
|
|
89
|
+
...(schema.description !== undefined && { description: schema.description }),
|
|
90
|
+
...(schema.strict !== undefined && { strict: schema.strict }),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { OpenAIResponseParser } from './response-parser';
|
|
3
|
+
import type { ILogger } from '@robota-sdk/agent-core';
|
|
4
|
+
import type OpenAI from 'openai';
|
|
5
|
+
|
|
6
|
+
function createMockLogger(): ILogger {
|
|
7
|
+
return {
|
|
8
|
+
info: vi.fn(),
|
|
9
|
+
warn: vi.fn(),
|
|
10
|
+
error: vi.fn(),
|
|
11
|
+
debug: vi.fn(),
|
|
12
|
+
log: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createChatCompletion(
|
|
17
|
+
overrides: Partial<OpenAI.Chat.ChatCompletion> = {},
|
|
18
|
+
): OpenAI.Chat.ChatCompletion {
|
|
19
|
+
return {
|
|
20
|
+
id: 'chatcmpl-test',
|
|
21
|
+
object: 'chat.completion',
|
|
22
|
+
created: Date.now(),
|
|
23
|
+
model: 'gpt-4',
|
|
24
|
+
choices: [
|
|
25
|
+
{
|
|
26
|
+
index: 0,
|
|
27
|
+
message: {
|
|
28
|
+
role: 'assistant',
|
|
29
|
+
content: 'Hello!',
|
|
30
|
+
refusal: null,
|
|
31
|
+
},
|
|
32
|
+
finish_reason: 'stop',
|
|
33
|
+
logprobs: null,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
...overrides,
|
|
37
|
+
} as OpenAI.Chat.ChatCompletion;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createStreamChunk(
|
|
41
|
+
overrides: Record<string, unknown> = {},
|
|
42
|
+
): OpenAI.Chat.ChatCompletionChunk {
|
|
43
|
+
return {
|
|
44
|
+
id: 'chatcmpl-chunk',
|
|
45
|
+
object: 'chat.completion.chunk',
|
|
46
|
+
created: Date.now(),
|
|
47
|
+
model: 'gpt-4',
|
|
48
|
+
choices: [
|
|
49
|
+
{
|
|
50
|
+
index: 0,
|
|
51
|
+
delta: { content: 'Hi' },
|
|
52
|
+
finish_reason: null,
|
|
53
|
+
logprobs: null,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
...overrides,
|
|
57
|
+
} as OpenAI.Chat.ChatCompletionChunk;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('OpenAIResponseParser', () => {
|
|
61
|
+
describe('constructor', () => {
|
|
62
|
+
it('should use SilentLogger when no logger is provided', () => {
|
|
63
|
+
const parser = new OpenAIResponseParser();
|
|
64
|
+
// Parser should work without throwing
|
|
65
|
+
const response = createChatCompletion();
|
|
66
|
+
const result = parser.parseResponse(response);
|
|
67
|
+
expect(result.role).toBe('assistant');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should accept a custom logger', () => {
|
|
71
|
+
const logger = createMockLogger();
|
|
72
|
+
const parser = new OpenAIResponseParser(logger);
|
|
73
|
+
const response = createChatCompletion();
|
|
74
|
+
const result = parser.parseResponse(response);
|
|
75
|
+
expect(result.role).toBe('assistant');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('parseResponse', () => {
|
|
80
|
+
it('should parse a simple text response', () => {
|
|
81
|
+
const parser = new OpenAIResponseParser();
|
|
82
|
+
const response = createChatCompletion();
|
|
83
|
+
const result = parser.parseResponse(response);
|
|
84
|
+
|
|
85
|
+
expect(result.role).toBe('assistant');
|
|
86
|
+
expect(result.content).toBe('Hello!');
|
|
87
|
+
expect(result.timestamp).toBeInstanceOf(Date);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should parse response with empty content as empty string', () => {
|
|
91
|
+
const parser = new OpenAIResponseParser();
|
|
92
|
+
const response = createChatCompletion({
|
|
93
|
+
choices: [
|
|
94
|
+
{
|
|
95
|
+
index: 0,
|
|
96
|
+
message: { role: 'assistant', content: null, refusal: null },
|
|
97
|
+
finish_reason: 'stop',
|
|
98
|
+
logprobs: null,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
const result = parser.parseResponse(response);
|
|
103
|
+
expect(result.content).toBe('');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should parse response with tool calls', () => {
|
|
107
|
+
const parser = new OpenAIResponseParser();
|
|
108
|
+
const response = createChatCompletion({
|
|
109
|
+
choices: [
|
|
110
|
+
{
|
|
111
|
+
index: 0,
|
|
112
|
+
message: {
|
|
113
|
+
role: 'assistant',
|
|
114
|
+
content: null,
|
|
115
|
+
refusal: null,
|
|
116
|
+
tool_calls: [
|
|
117
|
+
{
|
|
118
|
+
id: 'call_abc',
|
|
119
|
+
type: 'function',
|
|
120
|
+
function: {
|
|
121
|
+
name: 'calculate',
|
|
122
|
+
arguments: '{"a":1,"b":2}',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
finish_reason: 'tool_calls',
|
|
128
|
+
logprobs: null,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const result = parser.parseResponse(response);
|
|
134
|
+
expect(result.role).toBe('assistant');
|
|
135
|
+
expect(result.content).toBe('');
|
|
136
|
+
expect(result).toHaveProperty('toolCalls');
|
|
137
|
+
const assistantResult = result as {
|
|
138
|
+
toolCalls: Array<{
|
|
139
|
+
id: string;
|
|
140
|
+
type: string;
|
|
141
|
+
function: { name: string; arguments: string };
|
|
142
|
+
}>;
|
|
143
|
+
};
|
|
144
|
+
expect(assistantResult.toolCalls).toHaveLength(1);
|
|
145
|
+
expect(assistantResult.toolCalls[0].id).toBe('call_abc');
|
|
146
|
+
expect(assistantResult.toolCalls[0].function.name).toBe('calculate');
|
|
147
|
+
expect(assistantResult.toolCalls[0].function.arguments).toBe('{"a":1,"b":2}');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should parse response with usage information', () => {
|
|
151
|
+
const parser = new OpenAIResponseParser();
|
|
152
|
+
const response = createChatCompletion({
|
|
153
|
+
usage: {
|
|
154
|
+
prompt_tokens: 10,
|
|
155
|
+
completion_tokens: 20,
|
|
156
|
+
total_tokens: 30,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const result = parser.parseResponse(response);
|
|
161
|
+
expect(result).toHaveProperty('usage');
|
|
162
|
+
const withUsage = result as unknown as {
|
|
163
|
+
usage: { promptTokens: number; completionTokens: number; totalTokens: number };
|
|
164
|
+
};
|
|
165
|
+
expect(withUsage.usage.promptTokens).toBe(10);
|
|
166
|
+
expect(withUsage.usage.completionTokens).toBe(20);
|
|
167
|
+
expect(withUsage.usage.totalTokens).toBe(30);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should include finishReason in metadata', () => {
|
|
171
|
+
const parser = new OpenAIResponseParser();
|
|
172
|
+
const response = createChatCompletion();
|
|
173
|
+
const result = parser.parseResponse(response);
|
|
174
|
+
expect(result.metadata?.finishReason).toBe('stop');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should throw when response has no choices', () => {
|
|
178
|
+
const parser = new OpenAIResponseParser();
|
|
179
|
+
const response = createChatCompletion({ choices: [] });
|
|
180
|
+
|
|
181
|
+
expect(() => parser.parseResponse(response)).toThrow('OpenAI response parsing failed');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should throw with descriptive error and log on parsing failure', () => {
|
|
185
|
+
const logger = createMockLogger();
|
|
186
|
+
const parser = new OpenAIResponseParser(logger);
|
|
187
|
+
const response = createChatCompletion({ choices: [] });
|
|
188
|
+
|
|
189
|
+
expect(() => parser.parseResponse(response)).toThrow(
|
|
190
|
+
'OpenAI response parsing failed: No choices found in OpenAI response',
|
|
191
|
+
);
|
|
192
|
+
expect(logger.error).toHaveBeenCalled();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should handle response without usage field', () => {
|
|
196
|
+
const parser = new OpenAIResponseParser();
|
|
197
|
+
const response = createChatCompletion();
|
|
198
|
+
// Default createChatCompletion has no usage
|
|
199
|
+
const result = parser.parseResponse(response);
|
|
200
|
+
expect(result).not.toHaveProperty('usage');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should parse response with multiple tool calls', () => {
|
|
204
|
+
const parser = new OpenAIResponseParser();
|
|
205
|
+
const response = createChatCompletion({
|
|
206
|
+
choices: [
|
|
207
|
+
{
|
|
208
|
+
index: 0,
|
|
209
|
+
message: {
|
|
210
|
+
role: 'assistant',
|
|
211
|
+
content: null,
|
|
212
|
+
refusal: null,
|
|
213
|
+
tool_calls: [
|
|
214
|
+
{
|
|
215
|
+
id: 'call_1',
|
|
216
|
+
type: 'function',
|
|
217
|
+
function: { name: 'fn1', arguments: '{}' },
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: 'call_2',
|
|
221
|
+
type: 'function',
|
|
222
|
+
function: { name: 'fn2', arguments: '{"x":1}' },
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
finish_reason: 'tool_calls',
|
|
227
|
+
logprobs: null,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const result = parser.parseResponse(response);
|
|
233
|
+
const assistantResult = result as { toolCalls: Array<{ id: string }> };
|
|
234
|
+
expect(assistantResult.toolCalls).toHaveLength(2);
|
|
235
|
+
expect(assistantResult.toolCalls[0].id).toBe('call_1');
|
|
236
|
+
expect(assistantResult.toolCalls[1].id).toBe('call_2');
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('parseStreamingChunk', () => {
|
|
241
|
+
it('should parse a chunk with content', () => {
|
|
242
|
+
const parser = new OpenAIResponseParser();
|
|
243
|
+
const chunk = createStreamChunk();
|
|
244
|
+
const result = parser.parseStreamingChunk(chunk);
|
|
245
|
+
|
|
246
|
+
expect(result).not.toBeNull();
|
|
247
|
+
expect(result!.role).toBe('assistant');
|
|
248
|
+
expect(result!.content).toBe('Hi');
|
|
249
|
+
expect(result!.metadata?.isStreamChunk).toBe(true);
|
|
250
|
+
expect(result!.metadata?.isComplete).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should return null when chunk has no choices', () => {
|
|
254
|
+
const parser = new OpenAIResponseParser();
|
|
255
|
+
const chunk = createStreamChunk({ choices: [] });
|
|
256
|
+
const result = parser.parseStreamingChunk(chunk);
|
|
257
|
+
expect(result).toBeNull();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should handle chunk with empty content', () => {
|
|
261
|
+
const parser = new OpenAIResponseParser();
|
|
262
|
+
const chunk = createStreamChunk({
|
|
263
|
+
choices: [
|
|
264
|
+
{
|
|
265
|
+
index: 0,
|
|
266
|
+
delta: { content: '' },
|
|
267
|
+
finish_reason: null,
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
});
|
|
271
|
+
const result = parser.parseStreamingChunk(chunk);
|
|
272
|
+
expect(result).not.toBeNull();
|
|
273
|
+
expect(result!.content).toBe('');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should mark chunk as complete when finish_reason is stop', () => {
|
|
277
|
+
const parser = new OpenAIResponseParser();
|
|
278
|
+
const chunk = createStreamChunk({
|
|
279
|
+
choices: [
|
|
280
|
+
{
|
|
281
|
+
index: 0,
|
|
282
|
+
delta: { content: '' },
|
|
283
|
+
finish_reason: 'stop',
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
});
|
|
287
|
+
const result = parser.parseStreamingChunk(chunk);
|
|
288
|
+
expect(result!.metadata?.isComplete).toBe(true);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should mark chunk as complete when finish_reason is tool_calls', () => {
|
|
292
|
+
const parser = new OpenAIResponseParser();
|
|
293
|
+
const chunk = createStreamChunk({
|
|
294
|
+
choices: [
|
|
295
|
+
{
|
|
296
|
+
index: 0,
|
|
297
|
+
delta: { content: '' },
|
|
298
|
+
finish_reason: 'tool_calls',
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
});
|
|
302
|
+
const result = parser.parseStreamingChunk(chunk);
|
|
303
|
+
expect(result!.metadata?.isComplete).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should parse chunk with tool calls', () => {
|
|
307
|
+
const parser = new OpenAIResponseParser();
|
|
308
|
+
const chunk = createStreamChunk({
|
|
309
|
+
choices: [
|
|
310
|
+
{
|
|
311
|
+
index: 0,
|
|
312
|
+
delta: {
|
|
313
|
+
tool_calls: [
|
|
314
|
+
{
|
|
315
|
+
index: 0,
|
|
316
|
+
id: 'call_stream_1',
|
|
317
|
+
type: 'function',
|
|
318
|
+
function: {
|
|
319
|
+
name: 'search',
|
|
320
|
+
arguments: '{"q":"test"}',
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
},
|
|
325
|
+
finish_reason: null,
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const result = parser.parseStreamingChunk(chunk);
|
|
331
|
+
expect(result).not.toBeNull();
|
|
332
|
+
expect(result!.role).toBe('assistant');
|
|
333
|
+
expect(result!.content).toBe('');
|
|
334
|
+
const withToolCalls = result as unknown as {
|
|
335
|
+
toolCalls: Array<{ id: string; function: { name: string; arguments: string } }>;
|
|
336
|
+
};
|
|
337
|
+
expect(withToolCalls.toolCalls).toHaveLength(1);
|
|
338
|
+
expect(withToolCalls.toolCalls[0].id).toBe('call_stream_1');
|
|
339
|
+
expect(withToolCalls.toolCalls[0].function.name).toBe('search');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should handle chunk with tool calls missing optional fields', () => {
|
|
343
|
+
const parser = new OpenAIResponseParser();
|
|
344
|
+
const chunk = createStreamChunk({
|
|
345
|
+
choices: [
|
|
346
|
+
{
|
|
347
|
+
index: 0,
|
|
348
|
+
delta: {
|
|
349
|
+
tool_calls: [
|
|
350
|
+
{
|
|
351
|
+
index: 0,
|
|
352
|
+
// Missing id, function.name, function.arguments
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
},
|
|
356
|
+
finish_reason: null,
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const result = parser.parseStreamingChunk(chunk);
|
|
362
|
+
expect(result).not.toBeNull();
|
|
363
|
+
const withToolCalls = result as unknown as {
|
|
364
|
+
toolCalls: Array<{ id: string; function: { name: string; arguments: string } }>;
|
|
365
|
+
};
|
|
366
|
+
expect(withToolCalls.toolCalls[0].id).toBe('');
|
|
367
|
+
expect(withToolCalls.toolCalls[0].function.name).toBe('');
|
|
368
|
+
expect(withToolCalls.toolCalls[0].function.arguments).toBe('');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('should handle chunk with no delta content (undefined)', () => {
|
|
372
|
+
const parser = new OpenAIResponseParser();
|
|
373
|
+
const chunk = createStreamChunk({
|
|
374
|
+
choices: [
|
|
375
|
+
{
|
|
376
|
+
index: 0,
|
|
377
|
+
delta: {},
|
|
378
|
+
finish_reason: null,
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
});
|
|
382
|
+
const result = parser.parseStreamingChunk(chunk);
|
|
383
|
+
expect(result).not.toBeNull();
|
|
384
|
+
expect(result!.content).toBe('');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('should throw with descriptive error on parsing failure', () => {
|
|
388
|
+
const logger = createMockLogger();
|
|
389
|
+
const parser = new OpenAIResponseParser(logger);
|
|
390
|
+
|
|
391
|
+
// Force an error by passing a chunk that causes internal error
|
|
392
|
+
const badChunk = {
|
|
393
|
+
choices: [
|
|
394
|
+
{
|
|
395
|
+
get delta(): never {
|
|
396
|
+
throw new Error('Access error');
|
|
397
|
+
},
|
|
398
|
+
finish_reason: null,
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
} as unknown as OpenAI.Chat.ChatCompletionChunk;
|
|
402
|
+
|
|
403
|
+
expect(() => parser.parseStreamingChunk(badChunk)).toThrow('OpenAI chunk parsing failed');
|
|
404
|
+
expect(logger.error).toHaveBeenCalled();
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
});
|