@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,153 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IGemmaConsumedPseudoBlock,
|
|
3
|
+
IGemmaConsumedPseudoToolTag,
|
|
4
|
+
IGemmaParsedPseudoTag,
|
|
5
|
+
IGemmaPseudoProjectionOptions,
|
|
6
|
+
TGemmaJsonValue,
|
|
7
|
+
} from './pseudo-tool-call-types';
|
|
8
|
+
|
|
9
|
+
const XML_START_MARKER = '<';
|
|
10
|
+
|
|
11
|
+
export function createGemmaPseudoStartMarkers(toolNames: readonly string[]): string[] {
|
|
12
|
+
void toolNames;
|
|
13
|
+
return [XML_START_MARKER];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function findNextGemmaPseudoStartMarker(
|
|
17
|
+
text: string,
|
|
18
|
+
cursor: number,
|
|
19
|
+
markers: readonly string[],
|
|
20
|
+
): number {
|
|
21
|
+
void markers;
|
|
22
|
+
return text.indexOf(XML_START_MARKER, cursor);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function longestGemmaPseudoStartPrefixSuffixLength(
|
|
26
|
+
text: string,
|
|
27
|
+
markers: readonly string[],
|
|
28
|
+
): number {
|
|
29
|
+
void markers;
|
|
30
|
+
return text.endsWith(XML_START_MARKER) ? XML_START_MARKER.length : 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parseGemmaPseudoTag(
|
|
34
|
+
text: string,
|
|
35
|
+
start: number,
|
|
36
|
+
): IGemmaParsedPseudoTag | undefined {
|
|
37
|
+
const tagEnd = text.indexOf('>', start + 1);
|
|
38
|
+
if (tagEnd === -1) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const rawOpenTag = text.slice(start, tagEnd + 1);
|
|
43
|
+
const tagMatch = rawOpenTag.match(/^<\s*([A-Za-z][\w:-]*)([\s/>][\s\S]*?|)>$/);
|
|
44
|
+
if (!tagMatch) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const tagName = tagMatch[1] ?? '';
|
|
49
|
+
return {
|
|
50
|
+
tagName,
|
|
51
|
+
normalizedName: tagName.toLowerCase(),
|
|
52
|
+
rawOpenTag,
|
|
53
|
+
attributes: parseAttributes(tagMatch[2] ?? ''),
|
|
54
|
+
openEnd: tagEnd + 1,
|
|
55
|
+
selfClosing: /\/\s*>$/.test(rawOpenTag),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function consumeGemmaPseudoControlBlock(
|
|
60
|
+
text: string,
|
|
61
|
+
tag: IGemmaParsedPseudoTag,
|
|
62
|
+
options: IGemmaPseudoProjectionOptions,
|
|
63
|
+
): IGemmaConsumedPseudoBlock {
|
|
64
|
+
if (tag.selfClosing) {
|
|
65
|
+
return {
|
|
66
|
+
innerText: '',
|
|
67
|
+
end: tag.openEnd,
|
|
68
|
+
complete: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const closeStart = indexOfClosingTag(text, tag.tagName, tag.openEnd);
|
|
73
|
+
if (closeStart === -1) {
|
|
74
|
+
return {
|
|
75
|
+
innerText: text.slice(tag.openEnd),
|
|
76
|
+
end: options.final ? text.length : tag.openEnd - tag.rawOpenTag.length,
|
|
77
|
+
complete: false,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const closingTagEnd = text.indexOf('>', closeStart);
|
|
82
|
+
return {
|
|
83
|
+
innerText: text.slice(tag.openEnd, closeStart),
|
|
84
|
+
end: closingTagEnd === -1 ? text.length : closingTagEnd + 1,
|
|
85
|
+
complete: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function consumeGemmaPseudoToolTag(
|
|
90
|
+
text: string,
|
|
91
|
+
tag: IGemmaParsedPseudoTag,
|
|
92
|
+
): IGemmaConsumedPseudoToolTag {
|
|
93
|
+
if (tag.selfClosing) {
|
|
94
|
+
return { rawText: tag.rawOpenTag, end: tag.openEnd };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const closeStart = indexOfClosingTag(text, tag.tagName, tag.openEnd);
|
|
98
|
+
if (closeStart === -1) {
|
|
99
|
+
return { rawText: tag.rawOpenTag, end: tag.openEnd };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const closingTagEnd = text.indexOf('>', closeStart);
|
|
103
|
+
const end = closingTagEnd === -1 ? text.length : closingTagEnd + 1;
|
|
104
|
+
return { rawText: text.slice(tag.openEnd - tag.rawOpenTag.length, end), end };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function findGemmaDeclaredToolName(
|
|
108
|
+
tagName: string,
|
|
109
|
+
toolNames: readonly string[],
|
|
110
|
+
): string | undefined {
|
|
111
|
+
const normalizedTagName = normalizeToolName(tagName);
|
|
112
|
+
return toolNames.find((toolName) => normalizeToolName(toolName) === normalizedTagName);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseAttributes(attributeText: string): Record<string, TGemmaJsonValue> {
|
|
116
|
+
const attributes: Record<string, TGemmaJsonValue> = {};
|
|
117
|
+
const pattern = /([A-Za-z_][\w:-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
118
|
+
let match = pattern.exec(attributeText);
|
|
119
|
+
while (match) {
|
|
120
|
+
const key = match[1] ?? '';
|
|
121
|
+
const rawValue = match[2] ?? match[3] ?? '';
|
|
122
|
+
attributes[key] = parseAttributeValue(decodeXmlEntities(rawValue));
|
|
123
|
+
match = pattern.exec(attributeText);
|
|
124
|
+
}
|
|
125
|
+
return attributes;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseAttributeValue(value: string): TGemmaJsonValue {
|
|
129
|
+
if (value === 'true') return true;
|
|
130
|
+
if (value === 'false') return false;
|
|
131
|
+
if (value === 'null') return null;
|
|
132
|
+
if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)) {
|
|
133
|
+
return Number(value);
|
|
134
|
+
}
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function decodeXmlEntities(value: string): string {
|
|
139
|
+
return value
|
|
140
|
+
.replace(/"/g, '"')
|
|
141
|
+
.replace(/'/g, "'")
|
|
142
|
+
.replace(/</g, '<')
|
|
143
|
+
.replace(/>/g, '>')
|
|
144
|
+
.replace(/&/g, '&');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function indexOfClosingTag(text: string, tagName: string, cursor: number): number {
|
|
148
|
+
return text.toLowerCase().indexOf(`</${tagName.toLowerCase()}>`, cursor);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function normalizeToolName(value: string): string {
|
|
152
|
+
return value.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
|
153
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type TGemmaJsonValue =
|
|
2
|
+
| string
|
|
3
|
+
| number
|
|
4
|
+
| boolean
|
|
5
|
+
| null
|
|
6
|
+
| TGemmaJsonValue[]
|
|
7
|
+
| { [key: string]: TGemmaJsonValue };
|
|
8
|
+
|
|
9
|
+
export interface IGemmaPseudoProjectionOptions {
|
|
10
|
+
final: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface IGemmaParsedPseudoTag {
|
|
14
|
+
tagName: string;
|
|
15
|
+
normalizedName: string;
|
|
16
|
+
rawOpenTag: string;
|
|
17
|
+
attributes: Record<string, TGemmaJsonValue>;
|
|
18
|
+
openEnd: number;
|
|
19
|
+
selfClosing: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface IGemmaConsumedPseudoBlock {
|
|
23
|
+
innerText: string;
|
|
24
|
+
end: number;
|
|
25
|
+
complete: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface IGemmaConsumedPseudoToolTag {
|
|
29
|
+
rawText: string;
|
|
30
|
+
end: number;
|
|
31
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { GemmaReasoningProjector, projectGemmaReasoningText } from './index';
|
|
3
|
+
|
|
4
|
+
describe('Gemma reasoning projection', () => {
|
|
5
|
+
it('removes complete thought channel blocks from full text', () => {
|
|
6
|
+
const result = projectGemmaReasoningText(
|
|
7
|
+
'Hello <|channel>thought\nhidden reasoning<channel|>world',
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
expect(result.visibleText).toBe('Hello world');
|
|
11
|
+
expect(result.removedReasoning).toBe(true);
|
|
12
|
+
expect(result.rawText).toContain('hidden reasoning');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('removes malformed empty thought markers emitted by local templates', () => {
|
|
16
|
+
const result = projectGemmaReasoningText('<|channel>\n<channel|>thought\nFinal answer');
|
|
17
|
+
|
|
18
|
+
expect(result.visibleText).toBe('Final answer');
|
|
19
|
+
expect(result.removedReasoning).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('handles reasoning markers split across streamed deltas', () => {
|
|
23
|
+
const projector = new GemmaReasoningProjector();
|
|
24
|
+
|
|
25
|
+
expect(projector.project('<|cha')).toBe('');
|
|
26
|
+
expect(projector.project('nnel>thought\nhidden')).toBe('');
|
|
27
|
+
expect(projector.project(' reasoning<channel|>Visible')).toBe('Visible');
|
|
28
|
+
expect(projector.flush()).toBe('');
|
|
29
|
+
expect(projector.rawText).toContain('hidden reasoning');
|
|
30
|
+
expect(projector.removedReasoning).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('removes captured local channel markers split across background streamed deltas', () => {
|
|
34
|
+
const projector = new GemmaReasoningProjector();
|
|
35
|
+
const deltas = ['<|channel>', 's', '\n', '<channel|>', '으로'];
|
|
36
|
+
|
|
37
|
+
const visibleText =
|
|
38
|
+
deltas.map((delta) => projector.project(delta)).join('') + projector.flush();
|
|
39
|
+
|
|
40
|
+
expect(visibleText).toBe('으로');
|
|
41
|
+
expect(projector.rawText).toBe('<|channel>s\n<channel|>으로');
|
|
42
|
+
expect(projector.removedReasoning).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('preserves ordinary text that is not inside channel markers', () => {
|
|
46
|
+
const projector = new GemmaReasoningProjector();
|
|
47
|
+
|
|
48
|
+
expect(projector.project('A thought about code')).toBe('A thought about code');
|
|
49
|
+
expect(projector.flush()).toBe('');
|
|
50
|
+
expect(projector.removedReasoning).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const START_MARKER = '<|channel>';
|
|
2
|
+
const END_MARKER = '<channel|>';
|
|
3
|
+
const THOUGHT_LABEL = 'thought';
|
|
4
|
+
|
|
5
|
+
export interface IGemmaReasoningProjection {
|
|
6
|
+
rawText: string;
|
|
7
|
+
visibleText: string;
|
|
8
|
+
removedReasoning: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface IProjectionOptions {
|
|
12
|
+
final: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface IProjectionState {
|
|
16
|
+
visibleParts: string[];
|
|
17
|
+
removedReasoning: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function projectGemmaReasoningText(rawText: string): IGemmaReasoningProjection {
|
|
21
|
+
const result = projectText(rawText, { final: true });
|
|
22
|
+
return {
|
|
23
|
+
rawText,
|
|
24
|
+
visibleText: result.visibleParts.join(''),
|
|
25
|
+
removedReasoning: result.removedReasoning,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class GemmaReasoningProjector {
|
|
30
|
+
private buffer = '';
|
|
31
|
+
private emittedVisibleText = '';
|
|
32
|
+
private hasRemovedReasoning = false;
|
|
33
|
+
|
|
34
|
+
get rawText(): string {
|
|
35
|
+
return this.buffer;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get removedReasoning(): boolean {
|
|
39
|
+
return this.hasRemovedReasoning;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
project(delta: string): string {
|
|
43
|
+
if (delta.length === 0) {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.buffer += delta;
|
|
48
|
+
return this.projectVisibleText({ final: false });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
flush(): string {
|
|
52
|
+
return this.projectVisibleText({ final: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private projectVisibleText(options: IProjectionOptions): string {
|
|
56
|
+
const result = projectText(this.buffer, options);
|
|
57
|
+
this.hasRemovedReasoning = this.hasRemovedReasoning || result.removedReasoning;
|
|
58
|
+
|
|
59
|
+
const nextVisibleText = result.visibleParts.join('');
|
|
60
|
+
const delta = nextVisibleText.slice(this.emittedVisibleText.length);
|
|
61
|
+
this.emittedVisibleText = nextVisibleText;
|
|
62
|
+
return delta;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function projectText(rawText: string, options: IProjectionOptions): IProjectionState {
|
|
67
|
+
const state: IProjectionState = {
|
|
68
|
+
visibleParts: [],
|
|
69
|
+
removedReasoning: false,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
let cursor = 0;
|
|
73
|
+
while (cursor < rawText.length) {
|
|
74
|
+
const nextMarker = rawText.indexOf(START_MARKER, cursor);
|
|
75
|
+
if (nextMarker === -1) {
|
|
76
|
+
appendVisibleTail(state, rawText.slice(cursor), options);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
appendVisibleTail(state, rawText.slice(cursor, nextMarker), options);
|
|
81
|
+
const afterStart = nextMarker + START_MARKER.length;
|
|
82
|
+
const markerEnd = rawText.indexOf(END_MARKER, afterStart);
|
|
83
|
+
|
|
84
|
+
if (markerEnd === -1) {
|
|
85
|
+
if (options.final) {
|
|
86
|
+
state.removedReasoning = true;
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
state.removedReasoning = true;
|
|
92
|
+
cursor = consumeChannelBlock(rawText, afterStart, markerEnd);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return state;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function appendVisibleTail(
|
|
99
|
+
state: IProjectionState,
|
|
100
|
+
tail: string,
|
|
101
|
+
options: IProjectionOptions,
|
|
102
|
+
): void {
|
|
103
|
+
if (tail.length === 0) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (options.final) {
|
|
108
|
+
state.visibleParts.push(tail);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const heldLength = longestStartMarkerPrefixSuffixLength(tail);
|
|
113
|
+
state.visibleParts.push(tail.slice(0, tail.length - heldLength));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function consumeChannelBlock(rawText: string, afterStart: number, markerEnd: number): number {
|
|
117
|
+
const channelText = rawText.slice(afterStart, markerEnd);
|
|
118
|
+
let cursor = markerEnd + END_MARKER.length;
|
|
119
|
+
|
|
120
|
+
if (channelText.trim().length === 0) {
|
|
121
|
+
const followingThoughtLabel = rawText.slice(cursor).match(/^thought(?:\r?\n)*/);
|
|
122
|
+
if (followingThoughtLabel) {
|
|
123
|
+
cursor += followingThoughtLabel[0].length;
|
|
124
|
+
}
|
|
125
|
+
return cursor;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const channelLabel = channelText.split(/\r?\n/, 1)[0]?.trim();
|
|
129
|
+
if (channelLabel === THOUGHT_LABEL) {
|
|
130
|
+
return cursor;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return cursor;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function longestStartMarkerPrefixSuffixLength(text: string): number {
|
|
137
|
+
const maxLength = Math.min(text.length, START_MARKER.length - 1);
|
|
138
|
+
for (let length = maxLength; length > 0; length -= 1) {
|
|
139
|
+
if (START_MARKER.startsWith(text.slice(text.length - length))) {
|
|
140
|
+
return length;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type OpenAI from 'openai';
|
|
2
|
+
import { isAssistantMessage } from '@robota-sdk/agent-core';
|
|
3
|
+
import type { ILogger, IToolSchema, TUniversalMessage } from '@robota-sdk/agent-core';
|
|
4
|
+
import { OpenAICompatibleResponseParser } from '../shared/openai-compatible/index.js';
|
|
5
|
+
import type { IOpenAICompatibleToolCallTextProjector } from '../shared/openai-compatible/index.js';
|
|
6
|
+
import { createStreamTextMessage, createStreamToolCallMessage } from './message-factory';
|
|
7
|
+
import { GemmaReasoningProjector } from './reasoning-projector';
|
|
8
|
+
import { createGemmaToolCallProjector } from './tool-call-projector';
|
|
9
|
+
|
|
10
|
+
export interface IGemmaStreamProjectionState {
|
|
11
|
+
reasoningProjector: GemmaReasoningProjector;
|
|
12
|
+
responseParser: OpenAICompatibleResponseParser;
|
|
13
|
+
toolCallProjector?: IOpenAICompatibleToolCallTextProjector;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createGemmaStreamProjectionState(
|
|
17
|
+
logger: ILogger,
|
|
18
|
+
tools: readonly IToolSchema[] | undefined,
|
|
19
|
+
): IGemmaStreamProjectionState {
|
|
20
|
+
return {
|
|
21
|
+
reasoningProjector: new GemmaReasoningProjector(),
|
|
22
|
+
responseParser: new OpenAICompatibleResponseParser({ logger }),
|
|
23
|
+
...(tools && { toolCallProjector: createGemmaToolCallProjector(tools) }),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function projectGemmaStreamChunk(
|
|
28
|
+
chunk: OpenAI.Chat.ChatCompletionChunk,
|
|
29
|
+
state: IGemmaStreamProjectionState,
|
|
30
|
+
): TUniversalMessage[] {
|
|
31
|
+
const choice = chunk.choices?.[0];
|
|
32
|
+
if (!choice) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const nativeToolCallMessage = state.responseParser.parseStreamingChunk(chunk);
|
|
37
|
+
if (nativeToolCallMessage && isAssistantMessage(nativeToolCallMessage)) {
|
|
38
|
+
if (nativeToolCallMessage.toolCalls?.length) {
|
|
39
|
+
return [nativeToolCallMessage];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return projectGemmaTextDelta(choice.delta.content || '', choice.finish_reason, state);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function flushGemmaStreamProjection(
|
|
47
|
+
state: IGemmaStreamProjectionState,
|
|
48
|
+
): TUniversalMessage[] {
|
|
49
|
+
const messages = projectGemmaTextDelta('', null, state, true);
|
|
50
|
+
const flushedContent = state.reasoningProjector.flush();
|
|
51
|
+
if (flushedContent.length > 0) {
|
|
52
|
+
messages.push(createStreamTextMessage(flushedContent, null));
|
|
53
|
+
}
|
|
54
|
+
return messages;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function projectGemmaTextDelta(
|
|
58
|
+
rawContent: string,
|
|
59
|
+
finishReason: OpenAI.Chat.ChatCompletionChunk.Choice['finish_reason'],
|
|
60
|
+
state: IGemmaStreamProjectionState,
|
|
61
|
+
flushToolProjector = false,
|
|
62
|
+
): TUniversalMessage[] {
|
|
63
|
+
const toolProjection = flushToolProjector
|
|
64
|
+
? state.toolCallProjector?.flush()
|
|
65
|
+
: state.toolCallProjector?.project(rawContent);
|
|
66
|
+
const messages: TUniversalMessage[] = [];
|
|
67
|
+
|
|
68
|
+
if (toolProjection?.toolCalls.length) {
|
|
69
|
+
messages.push(createStreamToolCallMessage(toolProjection.toolCalls, finishReason));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const contentAfterToolProjection = toolProjection?.visibleText ?? rawContent;
|
|
73
|
+
const visibleContent = state.reasoningProjector.project(contentAfterToolProjection);
|
|
74
|
+
if (visibleContent.length > 0) {
|
|
75
|
+
messages.push(createStreamTextMessage(visibleContent, finishReason));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return messages;
|
|
79
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const STRING_DELIMITER = '<|"|>';
|
|
2
|
+
|
|
3
|
+
export interface IGemmaArgumentObject {
|
|
4
|
+
[key: string]: TGemmaArgumentValue;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type TGemmaArgumentValue =
|
|
8
|
+
| string
|
|
9
|
+
| number
|
|
10
|
+
| boolean
|
|
11
|
+
| null
|
|
12
|
+
| IGemmaArgumentObject
|
|
13
|
+
| TGemmaArgumentValue[];
|
|
14
|
+
|
|
15
|
+
export class GemmaArgumentParser {
|
|
16
|
+
private cursor = 0;
|
|
17
|
+
|
|
18
|
+
constructor(private readonly source: string) {}
|
|
19
|
+
|
|
20
|
+
parse(): IGemmaArgumentObject | undefined {
|
|
21
|
+
const value = this.parseObject();
|
|
22
|
+
this.skipWhitespace();
|
|
23
|
+
if (this.cursor !== this.source.length) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private parseObject(): IGemmaArgumentObject | undefined {
|
|
30
|
+
if (!this.consume('{')) return undefined;
|
|
31
|
+
const result: IGemmaArgumentObject = {};
|
|
32
|
+
this.skipWhitespace();
|
|
33
|
+
if (this.consume('}')) return result;
|
|
34
|
+
|
|
35
|
+
while (this.cursor < this.source.length) {
|
|
36
|
+
const key = this.parseKey();
|
|
37
|
+
if (!key || !this.consume(':')) return undefined;
|
|
38
|
+
const value = this.parseValue();
|
|
39
|
+
if (value === undefined) return undefined;
|
|
40
|
+
result[key] = value;
|
|
41
|
+
this.skipWhitespace();
|
|
42
|
+
if (this.consume('}')) return result;
|
|
43
|
+
if (!this.consume(',')) return undefined;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private parseArray(): TGemmaArgumentValue[] | undefined {
|
|
49
|
+
if (!this.consume('[')) return undefined;
|
|
50
|
+
const result: TGemmaArgumentValue[] = [];
|
|
51
|
+
this.skipWhitespace();
|
|
52
|
+
if (this.consume(']')) return result;
|
|
53
|
+
|
|
54
|
+
while (this.cursor < this.source.length) {
|
|
55
|
+
const value = this.parseValue();
|
|
56
|
+
if (value === undefined) return undefined;
|
|
57
|
+
result.push(value);
|
|
58
|
+
this.skipWhitespace();
|
|
59
|
+
if (this.consume(']')) return result;
|
|
60
|
+
if (!this.consume(',')) return undefined;
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private parseValue(): TGemmaArgumentValue | undefined {
|
|
66
|
+
this.skipWhitespace();
|
|
67
|
+
if (this.source.startsWith(STRING_DELIMITER, this.cursor)) return this.parseString();
|
|
68
|
+
if (this.source.startsWith('{', this.cursor)) return this.parseObject();
|
|
69
|
+
if (this.source.startsWith('[', this.cursor)) return this.parseArray();
|
|
70
|
+
if (this.consumeLiteral('true')) return true;
|
|
71
|
+
if (this.consumeLiteral('false')) return false;
|
|
72
|
+
if (this.consumeLiteral('null')) return null;
|
|
73
|
+
return this.parseNumber();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private parseKey(): string | undefined {
|
|
77
|
+
this.skipWhitespace();
|
|
78
|
+
if (this.source.startsWith(STRING_DELIMITER, this.cursor)) return this.parseString();
|
|
79
|
+
const match = /^[A-Za-z_][A-Za-z0-9_-]*/.exec(this.source.slice(this.cursor));
|
|
80
|
+
if (!match) return undefined;
|
|
81
|
+
this.cursor += match[0].length;
|
|
82
|
+
this.skipWhitespace();
|
|
83
|
+
return match[0];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private parseString(): string | undefined {
|
|
87
|
+
if (!this.consume(STRING_DELIMITER)) return undefined;
|
|
88
|
+
const end = this.source.indexOf(STRING_DELIMITER, this.cursor);
|
|
89
|
+
if (end === -1) return undefined;
|
|
90
|
+
const value = this.source.slice(this.cursor, end);
|
|
91
|
+
this.cursor = end + STRING_DELIMITER.length;
|
|
92
|
+
this.skipWhitespace();
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private parseNumber(): number | undefined {
|
|
97
|
+
const match = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(
|
|
98
|
+
this.source.slice(this.cursor),
|
|
99
|
+
);
|
|
100
|
+
if (!match) return undefined;
|
|
101
|
+
this.cursor += match[0].length;
|
|
102
|
+
this.skipWhitespace();
|
|
103
|
+
return Number(match[0]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private consume(expected: string): boolean {
|
|
107
|
+
this.skipWhitespace();
|
|
108
|
+
if (!this.source.startsWith(expected, this.cursor)) return false;
|
|
109
|
+
this.cursor += expected.length;
|
|
110
|
+
this.skipWhitespace();
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private consumeLiteral(expected: string): boolean {
|
|
115
|
+
if (!this.source.startsWith(expected, this.cursor)) return false;
|
|
116
|
+
this.cursor += expected.length;
|
|
117
|
+
this.skipWhitespace();
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private skipWhitespace(): void {
|
|
122
|
+
while (/\s/.test(this.source[this.cursor] ?? '')) {
|
|
123
|
+
this.cursor += 1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|