@vibe-lang/runtime 0.2.5
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/package.json +46 -0
- package/src/ast/index.ts +375 -0
- package/src/ast.ts +2 -0
- package/src/debug/advanced-features.ts +482 -0
- package/src/debug/bun-inspector.ts +424 -0
- package/src/debug/handoff-manager.ts +283 -0
- package/src/debug/index.ts +150 -0
- package/src/debug/runner.ts +365 -0
- package/src/debug/server.ts +565 -0
- package/src/debug/stack-merger.ts +267 -0
- package/src/debug/state.ts +581 -0
- package/src/debug/test/advanced-features.test.ts +300 -0
- package/src/debug/test/e2e.test.ts +218 -0
- package/src/debug/test/handoff-manager.test.ts +256 -0
- package/src/debug/test/runner.test.ts +256 -0
- package/src/debug/test/stack-merger.test.ts +163 -0
- package/src/debug/test/state.test.ts +400 -0
- package/src/debug/test/ts-debug-integration.test.ts +374 -0
- package/src/debug/test/ts-import-tracker.test.ts +125 -0
- package/src/debug/test/ts-source-map.test.ts +169 -0
- package/src/debug/ts-import-tracker.ts +151 -0
- package/src/debug/ts-source-map.ts +171 -0
- package/src/errors/index.ts +124 -0
- package/src/index.ts +358 -0
- package/src/lexer/index.ts +348 -0
- package/src/lexer.ts +2 -0
- package/src/parser/index.ts +792 -0
- package/src/parser/parse.ts +45 -0
- package/src/parser/test/async.test.ts +248 -0
- package/src/parser/test/destructuring.test.ts +167 -0
- package/src/parser/test/do-expression.test.ts +486 -0
- package/src/parser/test/errors/do-expression.test.ts +95 -0
- package/src/parser/test/errors/error-locations.test.ts +230 -0
- package/src/parser/test/errors/invalid-expressions.test.ts +144 -0
- package/src/parser/test/errors/missing-tokens.test.ts +126 -0
- package/src/parser/test/errors/model-declaration.test.ts +185 -0
- package/src/parser/test/errors/nested-blocks.test.ts +226 -0
- package/src/parser/test/errors/unclosed-delimiters.test.ts +122 -0
- package/src/parser/test/errors/unexpected-tokens.test.ts +120 -0
- package/src/parser/test/import-export.test.ts +143 -0
- package/src/parser/test/literals.test.ts +404 -0
- package/src/parser/test/model-declaration.test.ts +161 -0
- package/src/parser/test/nested-blocks.test.ts +402 -0
- package/src/parser/test/parser.test.ts +743 -0
- package/src/parser/test/private.test.ts +136 -0
- package/src/parser/test/template-literal.test.ts +127 -0
- package/src/parser/test/tool-declaration.test.ts +302 -0
- package/src/parser/test/ts-block.test.ts +252 -0
- package/src/parser/test/type-annotations.test.ts +254 -0
- package/src/parser/visitor/helpers.ts +330 -0
- package/src/parser/visitor.ts +794 -0
- package/src/parser.ts +2 -0
- package/src/runtime/ai/cache-chunking.test.ts +69 -0
- package/src/runtime/ai/cache-chunking.ts +73 -0
- package/src/runtime/ai/client.ts +109 -0
- package/src/runtime/ai/context.ts +168 -0
- package/src/runtime/ai/formatters.ts +316 -0
- package/src/runtime/ai/index.ts +38 -0
- package/src/runtime/ai/language-ref.ts +38 -0
- package/src/runtime/ai/providers/anthropic.ts +253 -0
- package/src/runtime/ai/providers/google.ts +201 -0
- package/src/runtime/ai/providers/openai.ts +156 -0
- package/src/runtime/ai/retry.ts +100 -0
- package/src/runtime/ai/return-tools.ts +301 -0
- package/src/runtime/ai/test/client.test.ts +83 -0
- package/src/runtime/ai/test/formatters.test.ts +485 -0
- package/src/runtime/ai/test/retry.test.ts +137 -0
- package/src/runtime/ai/test/return-tools.test.ts +450 -0
- package/src/runtime/ai/test/tool-loop.test.ts +319 -0
- package/src/runtime/ai/test/tool-schema.test.ts +241 -0
- package/src/runtime/ai/tool-loop.ts +203 -0
- package/src/runtime/ai/tool-schema.ts +151 -0
- package/src/runtime/ai/types.ts +113 -0
- package/src/runtime/ai-logger.ts +255 -0
- package/src/runtime/ai-provider.ts +347 -0
- package/src/runtime/async/dependencies.ts +276 -0
- package/src/runtime/async/executor.ts +293 -0
- package/src/runtime/async/index.ts +43 -0
- package/src/runtime/async/scheduling.ts +163 -0
- package/src/runtime/async/test/dependencies.test.ts +284 -0
- package/src/runtime/async/test/executor.test.ts +388 -0
- package/src/runtime/context.ts +357 -0
- package/src/runtime/exec/ai.ts +139 -0
- package/src/runtime/exec/expressions.ts +475 -0
- package/src/runtime/exec/frames.ts +26 -0
- package/src/runtime/exec/functions.ts +305 -0
- package/src/runtime/exec/interpolation.ts +312 -0
- package/src/runtime/exec/statements.ts +604 -0
- package/src/runtime/exec/tools.ts +129 -0
- package/src/runtime/exec/typescript.ts +215 -0
- package/src/runtime/exec/variables.ts +279 -0
- package/src/runtime/index.ts +975 -0
- package/src/runtime/modules.ts +452 -0
- package/src/runtime/serialize.ts +103 -0
- package/src/runtime/state.ts +489 -0
- package/src/runtime/stdlib/core.ts +45 -0
- package/src/runtime/stdlib/directory.test.ts +156 -0
- package/src/runtime/stdlib/edit.test.ts +154 -0
- package/src/runtime/stdlib/fastEdit.test.ts +201 -0
- package/src/runtime/stdlib/glob.test.ts +106 -0
- package/src/runtime/stdlib/grep.test.ts +144 -0
- package/src/runtime/stdlib/index.ts +16 -0
- package/src/runtime/stdlib/readFile.test.ts +123 -0
- package/src/runtime/stdlib/tools/index.ts +707 -0
- package/src/runtime/stdlib/writeFile.test.ts +157 -0
- package/src/runtime/step.ts +969 -0
- package/src/runtime/test/ai-context.test.ts +1086 -0
- package/src/runtime/test/ai-result-object.test.ts +419 -0
- package/src/runtime/test/ai-tool-flow.test.ts +859 -0
- package/src/runtime/test/async-execution-order.test.ts +618 -0
- package/src/runtime/test/async-execution.test.ts +344 -0
- package/src/runtime/test/async-nested.test.ts +660 -0
- package/src/runtime/test/async-parallel-timing.test.ts +546 -0
- package/src/runtime/test/basic1.test.ts +154 -0
- package/src/runtime/test/binary-operators.test.ts +431 -0
- package/src/runtime/test/break-statement.test.ts +257 -0
- package/src/runtime/test/context-modes.test.ts +650 -0
- package/src/runtime/test/context.test.ts +466 -0
- package/src/runtime/test/core-functions.test.ts +228 -0
- package/src/runtime/test/e2e.test.ts +88 -0
- package/src/runtime/test/error-locations/error-locations.test.ts +80 -0
- package/src/runtime/test/error-locations/main-error.vibe +4 -0
- package/src/runtime/test/error-locations/main-import-error.vibe +3 -0
- package/src/runtime/test/error-locations/utils/helper.vibe +5 -0
- package/src/runtime/test/for-in.test.ts +312 -0
- package/src/runtime/test/helpers.ts +69 -0
- package/src/runtime/test/imports.test.ts +334 -0
- package/src/runtime/test/json-expressions.test.ts +232 -0
- package/src/runtime/test/literals.test.ts +372 -0
- package/src/runtime/test/logical-indexing.test.ts +478 -0
- package/src/runtime/test/member-methods.test.ts +324 -0
- package/src/runtime/test/model-config.test.ts +338 -0
- package/src/runtime/test/null-handling.test.ts +342 -0
- package/src/runtime/test/private-visibility.test.ts +332 -0
- package/src/runtime/test/runtime-state.test.ts +514 -0
- package/src/runtime/test/scoping.test.ts +370 -0
- package/src/runtime/test/string-interpolation.test.ts +354 -0
- package/src/runtime/test/template-literal.test.ts +181 -0
- package/src/runtime/test/tool-execution.test.ts +467 -0
- package/src/runtime/test/tool-schema-generation.test.ts +477 -0
- package/src/runtime/test/tostring.test.ts +210 -0
- package/src/runtime/test/ts-block.test.ts +594 -0
- package/src/runtime/test/ts-error-location.test.ts +231 -0
- package/src/runtime/test/types.test.ts +732 -0
- package/src/runtime/test/verbose-logger.test.ts +710 -0
- package/src/runtime/test/vibe-expression.test.ts +54 -0
- package/src/runtime/test/vibe-value-errors.test.ts +541 -0
- package/src/runtime/test/while.test.ts +232 -0
- package/src/runtime/tools/builtin.ts +30 -0
- package/src/runtime/tools/directory-tools.ts +70 -0
- package/src/runtime/tools/file-tools.ts +228 -0
- package/src/runtime/tools/index.ts +5 -0
- package/src/runtime/tools/registry.ts +48 -0
- package/src/runtime/tools/search-tools.ts +134 -0
- package/src/runtime/tools/security.ts +36 -0
- package/src/runtime/tools/system-tools.ts +312 -0
- package/src/runtime/tools/test/fixtures/base-types.ts +40 -0
- package/src/runtime/tools/test/fixtures/test-types.ts +132 -0
- package/src/runtime/tools/test/registry.test.ts +713 -0
- package/src/runtime/tools/test/security.test.ts +86 -0
- package/src/runtime/tools/test/system-tools.test.ts +679 -0
- package/src/runtime/tools/test/ts-schema.test.ts +357 -0
- package/src/runtime/tools/ts-schema.ts +341 -0
- package/src/runtime/tools/types.ts +89 -0
- package/src/runtime/tools/utility-tools.ts +198 -0
- package/src/runtime/ts-eval.ts +126 -0
- package/src/runtime/types.ts +797 -0
- package/src/runtime/validation.ts +160 -0
- package/src/runtime/verbose-logger.ts +459 -0
- package/src/runtime.ts +2 -0
- package/src/semantic/analyzer-context.ts +62 -0
- package/src/semantic/analyzer-validators.ts +575 -0
- package/src/semantic/analyzer-visitors.ts +534 -0
- package/src/semantic/analyzer.ts +83 -0
- package/src/semantic/index.ts +11 -0
- package/src/semantic/symbol-table.ts +58 -0
- package/src/semantic/test/async-validation.test.ts +301 -0
- package/src/semantic/test/compress-validation.test.ts +179 -0
- package/src/semantic/test/const-reassignment.test.ts +111 -0
- package/src/semantic/test/control-flow.test.ts +346 -0
- package/src/semantic/test/destructuring.test.ts +185 -0
- package/src/semantic/test/duplicate-declarations.test.ts +168 -0
- package/src/semantic/test/export-validation.test.ts +111 -0
- package/src/semantic/test/fixtures/math.ts +31 -0
- package/src/semantic/test/imports.test.ts +148 -0
- package/src/semantic/test/json-type.test.ts +68 -0
- package/src/semantic/test/literals.test.ts +127 -0
- package/src/semantic/test/model-validation.test.ts +179 -0
- package/src/semantic/test/prompt-validation.test.ts +343 -0
- package/src/semantic/test/scoping.test.ts +312 -0
- package/src/semantic/test/tool-validation.test.ts +306 -0
- package/src/semantic/test/ts-type-checking.test.ts +563 -0
- package/src/semantic/test/type-constraints.test.ts +111 -0
- package/src/semantic/test/type-inference.test.ts +87 -0
- package/src/semantic/test/type-validation.test.ts +552 -0
- package/src/semantic/test/undefined-variables.test.ts +163 -0
- package/src/semantic/ts-block-checker.ts +204 -0
- package/src/semantic/ts-signatures.ts +194 -0
- package/src/semantic/ts-types.ts +170 -0
- package/src/semantic/types.ts +58 -0
- package/tests/fixtures/conditional-logic.vibe +14 -0
- package/tests/fixtures/function-call.vibe +16 -0
- package/tests/fixtures/imports/cycle-detection/a.vibe +6 -0
- package/tests/fixtures/imports/cycle-detection/b.vibe +5 -0
- package/tests/fixtures/imports/cycle-detection/main.vibe +3 -0
- package/tests/fixtures/imports/module-isolation/main-b.vibe +8 -0
- package/tests/fixtures/imports/module-isolation/main.vibe +9 -0
- package/tests/fixtures/imports/module-isolation/moduleA.vibe +6 -0
- package/tests/fixtures/imports/module-isolation/moduleB.vibe +6 -0
- package/tests/fixtures/imports/nested-import/helper.vibe +6 -0
- package/tests/fixtures/imports/nested-import/main.vibe +3 -0
- package/tests/fixtures/imports/nested-import/utils.ts +3 -0
- package/tests/fixtures/imports/nested-isolation/file2.vibe +15 -0
- package/tests/fixtures/imports/nested-isolation/file3.vibe +10 -0
- package/tests/fixtures/imports/nested-isolation/main.vibe +21 -0
- package/tests/fixtures/imports/pure-cycle/a.vibe +5 -0
- package/tests/fixtures/imports/pure-cycle/b.vibe +5 -0
- package/tests/fixtures/imports/pure-cycle/main.vibe +3 -0
- package/tests/fixtures/imports/ts-boolean/checks.ts +14 -0
- package/tests/fixtures/imports/ts-boolean/main.vibe +10 -0
- package/tests/fixtures/imports/ts-boolean/type-mismatch.vibe +5 -0
- package/tests/fixtures/imports/ts-boolean/use-constant.vibe +18 -0
- package/tests/fixtures/imports/ts-error-handling/helpers.ts +42 -0
- package/tests/fixtures/imports/ts-error-handling/main.vibe +5 -0
- package/tests/fixtures/imports/ts-import/main.vibe +4 -0
- package/tests/fixtures/imports/ts-import/math.ts +9 -0
- package/tests/fixtures/imports/ts-variables/call-non-function.vibe +5 -0
- package/tests/fixtures/imports/ts-variables/data.ts +10 -0
- package/tests/fixtures/imports/ts-variables/import-json.vibe +5 -0
- package/tests/fixtures/imports/ts-variables/import-type-mismatch.vibe +5 -0
- package/tests/fixtures/imports/ts-variables/import-variable.vibe +5 -0
- package/tests/fixtures/imports/vibe-import/greet.vibe +5 -0
- package/tests/fixtures/imports/vibe-import/main.vibe +3 -0
- package/tests/fixtures/multiple-ai-calls.vibe +10 -0
- package/tests/fixtures/simple-greeting.vibe +6 -0
- package/tests/fixtures/template-literals.vibe +11 -0
- package/tests/integration/basic-ai/basic-ai.integration.test.ts +166 -0
- package/tests/integration/basic-ai/basic-ai.vibe +12 -0
- package/tests/integration/bug-fix/bug-fix.integration.test.ts +201 -0
- package/tests/integration/bug-fix/buggy-code.ts +22 -0
- package/tests/integration/bug-fix/fix-bug.vibe +21 -0
- package/tests/integration/compress/compress.integration.test.ts +206 -0
- package/tests/integration/destructuring/destructuring.integration.test.ts +92 -0
- package/tests/integration/hello-world-translator/hello-world-translator.integration.test.ts +61 -0
- package/tests/integration/line-annotator/context-modes.integration.test.ts +261 -0
- package/tests/integration/line-annotator/line-annotator.integration.test.ts +148 -0
- package/tests/integration/multi-feature/cumulative-sum.integration.test.ts +75 -0
- package/tests/integration/multi-feature/number-analyzer.integration.test.ts +191 -0
- package/tests/integration/multi-feature/number-analyzer.vibe +59 -0
- package/tests/integration/tool-calls/tool-calls.integration.test.ts +93 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Google Generative AI Provider Implementation using official SDK
|
|
2
|
+
|
|
3
|
+
import { GoogleGenAI } from '@google/genai';
|
|
4
|
+
import type { AIRequest, AIResponse, AIToolCall, ThinkingLevel } from '../types';
|
|
5
|
+
import { AIError } from '../types';
|
|
6
|
+
import { buildSystemMessage, buildContextMessage, buildPromptMessage, buildToolSystemMessage } from '../formatters';
|
|
7
|
+
import { toGoogleFunctionDeclarations } from '../tool-schema';
|
|
8
|
+
|
|
9
|
+
/** Map thinking level to Google Gemini 3 thinkingLevel */
|
|
10
|
+
const THINKING_LEVEL_MAP: Record<ThinkingLevel, string | null> = {
|
|
11
|
+
none: null, // Don't set thinkingConfig
|
|
12
|
+
low: 'low',
|
|
13
|
+
medium: 'medium',
|
|
14
|
+
high: 'high',
|
|
15
|
+
max: 'high', // Gemini 3 Flash max is 'high'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Generate a unique ID for tool calls (Google doesn't provide one) */
|
|
19
|
+
function generateToolCallId(): string {
|
|
20
|
+
return `call_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Execute an AI request using the Google Gen AI SDK.
|
|
25
|
+
*/
|
|
26
|
+
export async function executeGoogle(request: AIRequest): Promise<AIResponse> {
|
|
27
|
+
const { prompt, contextText, targetType, model, tools, previousToolCalls, toolResults } = request;
|
|
28
|
+
|
|
29
|
+
// Create Google Gen AI client
|
|
30
|
+
const client = new GoogleGenAI({ apiKey: model.apiKey });
|
|
31
|
+
|
|
32
|
+
// Build combined prompt (Google uses a simpler message format)
|
|
33
|
+
const baseSystemInstruction = buildSystemMessage();
|
|
34
|
+
const toolSystemMessage = tools?.length ? buildToolSystemMessage(tools) : null;
|
|
35
|
+
const systemInstruction = toolSystemMessage
|
|
36
|
+
? `${baseSystemInstruction}\n\n${toolSystemMessage}`
|
|
37
|
+
: baseSystemInstruction;
|
|
38
|
+
|
|
39
|
+
const contextMessage = buildContextMessage(contextText);
|
|
40
|
+
const promptMessage = buildPromptMessage(prompt);
|
|
41
|
+
|
|
42
|
+
// Combine into single prompt for Google
|
|
43
|
+
const parts: string[] = [];
|
|
44
|
+
if (contextMessage) parts.push(contextMessage);
|
|
45
|
+
parts.push(promptMessage);
|
|
46
|
+
const combinedPrompt = parts.join('\n\n');
|
|
47
|
+
|
|
48
|
+
// Build conversation contents - either simple prompt or multi-turn with tool results
|
|
49
|
+
type ContentPart = { text: string } | { functionCall: { name: string; args: Record<string, unknown> } } | { functionResponse: { name: string; response: unknown } };
|
|
50
|
+
type Content = { role: 'user' | 'model'; parts: ContentPart[] };
|
|
51
|
+
|
|
52
|
+
let contents: string | Content[];
|
|
53
|
+
|
|
54
|
+
if (previousToolCalls?.length && toolResults?.length) {
|
|
55
|
+
// Multi-turn conversation with tool results
|
|
56
|
+
// 1. Original user message
|
|
57
|
+
const userMessage: Content = {
|
|
58
|
+
role: 'user',
|
|
59
|
+
parts: [{ text: combinedPrompt }],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// 2. Model message with function calls (including thoughtSignature for Gemini 3)
|
|
63
|
+
const modelParts: ContentPart[] = previousToolCalls.map(call => {
|
|
64
|
+
const part: ContentPart = {
|
|
65
|
+
functionCall: {
|
|
66
|
+
name: call.toolName,
|
|
67
|
+
args: call.args,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
// Include thoughtSignature if present (required for Gemini 3)
|
|
71
|
+
if (call.thoughtSignature) {
|
|
72
|
+
(part as Record<string, unknown>).thoughtSignature = call.thoughtSignature;
|
|
73
|
+
}
|
|
74
|
+
return part;
|
|
75
|
+
});
|
|
76
|
+
const modelMessage: Content = {
|
|
77
|
+
role: 'model',
|
|
78
|
+
parts: modelParts,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// 3. User message with function responses
|
|
82
|
+
const responseParts: ContentPart[] = toolResults.map((result, i) => ({
|
|
83
|
+
functionResponse: {
|
|
84
|
+
name: previousToolCalls[i].toolName,
|
|
85
|
+
response: result.error
|
|
86
|
+
? { error: result.error }
|
|
87
|
+
: { result: result.result },
|
|
88
|
+
},
|
|
89
|
+
}));
|
|
90
|
+
const responseMessage: Content = {
|
|
91
|
+
role: 'user',
|
|
92
|
+
parts: responseParts,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
contents = [userMessage, modelMessage, responseMessage];
|
|
96
|
+
} else {
|
|
97
|
+
// Simple single prompt
|
|
98
|
+
contents = combinedPrompt;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// Build generation config
|
|
103
|
+
const generationConfig: Record<string, unknown> = {};
|
|
104
|
+
|
|
105
|
+
// Add thinking config if level specified
|
|
106
|
+
const thinkingLevel = model.thinkingLevel as ThinkingLevel | undefined;
|
|
107
|
+
const googleThinkingLevel = thinkingLevel ? THINKING_LEVEL_MAP[thinkingLevel] : null;
|
|
108
|
+
if (googleThinkingLevel) {
|
|
109
|
+
generationConfig.thinkingConfig = {
|
|
110
|
+
thinkingLevel: googleThinkingLevel,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Build config with optional tools
|
|
115
|
+
const config: Record<string, unknown> = {
|
|
116
|
+
systemInstruction,
|
|
117
|
+
...generationConfig,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Add tools if provided
|
|
121
|
+
if (tools?.length) {
|
|
122
|
+
config.tools = [{ functionDeclarations: toGoogleFunctionDeclarations(tools) }];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Make API request
|
|
126
|
+
// Cast contents to unknown to avoid strict SDK type checking (we build valid content)
|
|
127
|
+
const response = await client.models.generateContent({
|
|
128
|
+
model: model.name,
|
|
129
|
+
contents: contents as unknown as Parameters<typeof client.models.generateContent>[0]['contents'],
|
|
130
|
+
config,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Extract text content
|
|
134
|
+
const content = response.text ?? '';
|
|
135
|
+
|
|
136
|
+
// Extract function calls from response parts (including thoughtSignature for Gemini 3)
|
|
137
|
+
const responseParts = (response.candidates?.[0]?.content?.parts ?? []) as Array<{
|
|
138
|
+
text?: string;
|
|
139
|
+
functionCall?: { name: string; args: Record<string, unknown> };
|
|
140
|
+
thoughtSignature?: string;
|
|
141
|
+
}>;
|
|
142
|
+
const functionCallParts = responseParts.filter((p) => p.functionCall);
|
|
143
|
+
let toolCalls: AIToolCall[] | undefined;
|
|
144
|
+
if (functionCallParts.length > 0) {
|
|
145
|
+
toolCalls = functionCallParts.map((p) => ({
|
|
146
|
+
id: generateToolCallId(),
|
|
147
|
+
toolName: p.functionCall!.name,
|
|
148
|
+
args: p.functionCall!.args,
|
|
149
|
+
thoughtSignature: p.thoughtSignature,
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Determine stop reason
|
|
154
|
+
const finishReason = response.candidates?.[0]?.finishReason as string | undefined;
|
|
155
|
+
const stopReason =
|
|
156
|
+
finishReason === 'STOP'
|
|
157
|
+
? (toolCalls?.length ? 'tool_use' : 'end')
|
|
158
|
+
: finishReason === 'MAX_TOKENS'
|
|
159
|
+
? 'length'
|
|
160
|
+
: finishReason === 'SAFETY'
|
|
161
|
+
? 'content_filter'
|
|
162
|
+
: 'end';
|
|
163
|
+
|
|
164
|
+
// Extract usage from response including cached and thinking tokens
|
|
165
|
+
const meta = response.usageMetadata as Record<string, unknown> | undefined;
|
|
166
|
+
const usage = meta
|
|
167
|
+
? {
|
|
168
|
+
inputTokens: Number(meta.promptTokenCount ?? 0),
|
|
169
|
+
outputTokens: Number(meta.candidatesTokenCount ?? 0),
|
|
170
|
+
cachedInputTokens: meta.cachedContentTokenCount ? Number(meta.cachedContentTokenCount) : undefined,
|
|
171
|
+
thinkingTokens: meta.thoughtsTokenCount ? Number(meta.thoughtsTokenCount) : undefined,
|
|
172
|
+
}
|
|
173
|
+
: undefined;
|
|
174
|
+
|
|
175
|
+
// For text responses, parsedValue is just the content
|
|
176
|
+
// For typed responses, the value comes from return tool calls (handled by tool-loop)
|
|
177
|
+
return { content, parsedValue: content, usage, toolCalls, stopReason };
|
|
178
|
+
} catch (error) {
|
|
179
|
+
// Handle Google API errors
|
|
180
|
+
if (error instanceof Error) {
|
|
181
|
+
const message = error.message.toLowerCase();
|
|
182
|
+
const isRetryable =
|
|
183
|
+
message.includes('429') ||
|
|
184
|
+
message.includes('rate limit') ||
|
|
185
|
+
message.includes('500') ||
|
|
186
|
+
message.includes('503') ||
|
|
187
|
+
message.includes('service unavailable');
|
|
188
|
+
|
|
189
|
+
// Extract status code if present
|
|
190
|
+
const statusMatch = error.message.match(/(\d{3})/);
|
|
191
|
+
const statusCode = statusMatch ? parseInt(statusMatch[1]) : undefined;
|
|
192
|
+
|
|
193
|
+
throw new AIError(
|
|
194
|
+
`Google API error: ${error.message}`,
|
|
195
|
+
statusCode,
|
|
196
|
+
isRetryable
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// OpenAI Provider Implementation using official SDK
|
|
2
|
+
|
|
3
|
+
import OpenAI from 'openai';
|
|
4
|
+
import type { AIRequest, AIResponse, AIToolCall, ThinkingLevel } from '../types';
|
|
5
|
+
import { AIError } from '../types';
|
|
6
|
+
import { buildMessages } from '../formatters';
|
|
7
|
+
import { toOpenAITools } from '../tool-schema';
|
|
8
|
+
|
|
9
|
+
/** OpenAI provider configuration */
|
|
10
|
+
export const OPENAI_CONFIG = {
|
|
11
|
+
defaultUrl: 'https://api.openai.com/v1',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Map thinking level to OpenAI reasoning_effort */
|
|
15
|
+
const REASONING_EFFORT_MAP: Record<ThinkingLevel, string> = {
|
|
16
|
+
none: 'none',
|
|
17
|
+
low: 'low',
|
|
18
|
+
medium: 'medium',
|
|
19
|
+
high: 'high',
|
|
20
|
+
max: 'xhigh', // OpenAI uses 'xhigh' for max reasoning
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Execute an AI request using the OpenAI SDK.
|
|
25
|
+
*/
|
|
26
|
+
export async function executeOpenAI(request: AIRequest): Promise<AIResponse> {
|
|
27
|
+
const { prompt, contextText, targetType, model, tools, previousToolCalls, toolResults } = request;
|
|
28
|
+
|
|
29
|
+
// Create OpenAI client
|
|
30
|
+
const client = new OpenAI({
|
|
31
|
+
apiKey: model.apiKey,
|
|
32
|
+
baseURL: model.url ?? OPENAI_CONFIG.defaultUrl,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Build base messages
|
|
36
|
+
const baseMessages = buildMessages(prompt, contextText, tools);
|
|
37
|
+
|
|
38
|
+
// Build conversation messages - either simple or multi-turn with tool results
|
|
39
|
+
type ChatMessage = OpenAI.ChatCompletionMessageParam;
|
|
40
|
+
let messages: ChatMessage[] = baseMessages.map((m) => ({
|
|
41
|
+
role: m.role as 'system' | 'user' | 'assistant',
|
|
42
|
+
content: m.content,
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
// Add tool call history if present (multi-turn conversation)
|
|
46
|
+
if (previousToolCalls?.length && toolResults?.length) {
|
|
47
|
+
// Add assistant message with tool calls
|
|
48
|
+
const assistantMessage: ChatMessage = {
|
|
49
|
+
role: 'assistant',
|
|
50
|
+
content: null,
|
|
51
|
+
tool_calls: previousToolCalls.map(call => ({
|
|
52
|
+
id: call.id,
|
|
53
|
+
type: 'function' as const,
|
|
54
|
+
function: {
|
|
55
|
+
name: call.toolName,
|
|
56
|
+
arguments: JSON.stringify(call.args),
|
|
57
|
+
},
|
|
58
|
+
})),
|
|
59
|
+
};
|
|
60
|
+
messages.push(assistantMessage);
|
|
61
|
+
|
|
62
|
+
// Add tool result messages
|
|
63
|
+
for (let i = 0; i < toolResults.length; i++) {
|
|
64
|
+
const result = toolResults[i];
|
|
65
|
+
const call = previousToolCalls[i];
|
|
66
|
+
const toolMessage: ChatMessage = {
|
|
67
|
+
role: 'tool',
|
|
68
|
+
tool_call_id: call.id,
|
|
69
|
+
content: result.error
|
|
70
|
+
? `Error: ${result.error}`
|
|
71
|
+
: typeof result.result === 'string'
|
|
72
|
+
? result.result
|
|
73
|
+
: JSON.stringify(result.result),
|
|
74
|
+
};
|
|
75
|
+
messages.push(toolMessage);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Build request parameters
|
|
81
|
+
const params: OpenAI.ChatCompletionCreateParamsNonStreaming = {
|
|
82
|
+
model: model.name,
|
|
83
|
+
messages,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Add tools if provided
|
|
87
|
+
if (tools?.length) {
|
|
88
|
+
params.tools = toOpenAITools(tools) as OpenAI.ChatCompletionTool[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Add reasoning effort if thinking level specified
|
|
92
|
+
const thinkingLevel = model.thinkingLevel as ThinkingLevel | undefined;
|
|
93
|
+
if (thinkingLevel) {
|
|
94
|
+
(params as unknown as Record<string, unknown>).reasoning_effort = REASONING_EFFORT_MAP[thinkingLevel];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Make API request
|
|
98
|
+
const completion = await client.chat.completions.create(params);
|
|
99
|
+
|
|
100
|
+
// Extract message
|
|
101
|
+
const message = completion.choices[0]?.message;
|
|
102
|
+
const content = message?.content ?? '';
|
|
103
|
+
const finishReason = completion.choices[0]?.finish_reason;
|
|
104
|
+
|
|
105
|
+
// Extract usage including cached and reasoning tokens
|
|
106
|
+
const rawUsage = completion.usage as unknown as Record<string, unknown> | undefined;
|
|
107
|
+
const promptDetails = rawUsage?.prompt_tokens_details as Record<string, unknown> | undefined;
|
|
108
|
+
const completionDetails = rawUsage?.completion_tokens_details as Record<string, unknown> | undefined;
|
|
109
|
+
const usage = rawUsage
|
|
110
|
+
? {
|
|
111
|
+
inputTokens: Number(rawUsage.prompt_tokens ?? 0),
|
|
112
|
+
outputTokens: Number(rawUsage.completion_tokens ?? 0),
|
|
113
|
+
cachedInputTokens: promptDetails?.cached_tokens ? Number(promptDetails.cached_tokens) : undefined,
|
|
114
|
+
thinkingTokens: completionDetails?.reasoning_tokens ? Number(completionDetails.reasoning_tokens) : undefined,
|
|
115
|
+
}
|
|
116
|
+
: undefined;
|
|
117
|
+
|
|
118
|
+
// Parse tool calls if present
|
|
119
|
+
let toolCalls: AIToolCall[] | undefined;
|
|
120
|
+
if (message?.tool_calls?.length) {
|
|
121
|
+
toolCalls = message.tool_calls
|
|
122
|
+
.filter((tc): tc is OpenAI.ChatCompletionMessageToolCall & { function: { name: string; arguments: string } } =>
|
|
123
|
+
'function' in tc && tc.function !== undefined
|
|
124
|
+
)
|
|
125
|
+
.map((tc) => ({
|
|
126
|
+
id: tc.id,
|
|
127
|
+
toolName: tc.function.name,
|
|
128
|
+
args: JSON.parse(tc.function.arguments),
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Determine stop reason
|
|
133
|
+
const stopReason =
|
|
134
|
+
finishReason === 'tool_calls'
|
|
135
|
+
? 'tool_use'
|
|
136
|
+
: finishReason === 'length'
|
|
137
|
+
? 'length'
|
|
138
|
+
: finishReason === 'content_filter'
|
|
139
|
+
? 'content_filter'
|
|
140
|
+
: 'end';
|
|
141
|
+
|
|
142
|
+
// For text responses, parsedValue is just the content
|
|
143
|
+
// For typed responses, the value comes from return tool calls (handled by tool-loop)
|
|
144
|
+
return { content, parsedValue: content, usage, toolCalls, stopReason };
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error instanceof OpenAI.APIError) {
|
|
147
|
+
const isRetryable = error.status === 429 || (error.status ?? 0) >= 500;
|
|
148
|
+
throw new AIError(
|
|
149
|
+
`OpenAI API error (${error.status}): ${error.message}`,
|
|
150
|
+
error.status,
|
|
151
|
+
isRetryable
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Retry utilities for AI operations
|
|
2
|
+
|
|
3
|
+
import { AIError, type RetryOptions } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if an error is retryable (5xx, 429, or network error).
|
|
7
|
+
*/
|
|
8
|
+
export function isRetryableError(error: unknown): boolean {
|
|
9
|
+
if (error instanceof AIError) {
|
|
10
|
+
return error.isRetryable;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Network errors
|
|
14
|
+
if (error instanceof TypeError && error.message.includes('fetch')) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check for status codes
|
|
19
|
+
if (error instanceof Error) {
|
|
20
|
+
const message = error.message.toLowerCase();
|
|
21
|
+
// Network-related errors
|
|
22
|
+
if (
|
|
23
|
+
message.includes('network') ||
|
|
24
|
+
message.includes('timeout') ||
|
|
25
|
+
message.includes('econnreset') ||
|
|
26
|
+
message.includes('econnrefused') ||
|
|
27
|
+
message.includes('socket')
|
|
28
|
+
) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Calculate delay for exponential backoff with jitter.
|
|
38
|
+
* Formula: min(maxDelay, baseDelay * 2^attempt) * (0.5 + random(0.5))
|
|
39
|
+
*/
|
|
40
|
+
export function calculateDelay(
|
|
41
|
+
attempt: number,
|
|
42
|
+
baseDelayMs = 1000,
|
|
43
|
+
maxDelayMs = 30000
|
|
44
|
+
): number {
|
|
45
|
+
const exponentialDelay = baseDelayMs * Math.pow(2, attempt);
|
|
46
|
+
const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
|
|
47
|
+
// Add jitter: 50% to 100% of the delay
|
|
48
|
+
const jitter = 0.5 + Math.random() * 0.5;
|
|
49
|
+
return Math.floor(cappedDelay * jitter);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Sleep for a given number of milliseconds.
|
|
54
|
+
*/
|
|
55
|
+
function sleep(ms: number): Promise<void> {
|
|
56
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Execute a function with retry logic.
|
|
61
|
+
* Retries on 5xx, 429, and network errors with exponential backoff.
|
|
62
|
+
*/
|
|
63
|
+
export async function withRetry<T>(
|
|
64
|
+
fn: () => Promise<T>,
|
|
65
|
+
options: RetryOptions
|
|
66
|
+
): Promise<T> {
|
|
67
|
+
const { maxRetries, baseDelayMs = 1000, maxDelayMs = 30000 } = options;
|
|
68
|
+
let lastError: Error | null = null;
|
|
69
|
+
|
|
70
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
71
|
+
try {
|
|
72
|
+
return await fn();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
75
|
+
|
|
76
|
+
// Check if error is retryable and we have attempts left
|
|
77
|
+
if (!isRetryableError(error) || attempt >= maxRetries) {
|
|
78
|
+
throw lastError;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Wait before retrying
|
|
82
|
+
const delay = calculateDelay(attempt, baseDelayMs, maxDelayMs);
|
|
83
|
+
await sleep(delay);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// This should never be reached, but TypeScript needs it
|
|
88
|
+
throw lastError ?? new Error('Retry failed');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create an AIError from an HTTP response.
|
|
93
|
+
*/
|
|
94
|
+
export function createAIErrorFromResponse(
|
|
95
|
+
status: number,
|
|
96
|
+
message: string
|
|
97
|
+
): AIError {
|
|
98
|
+
const isRetryable = status === 429 || (status >= 500 && status < 600);
|
|
99
|
+
return new AIError(message, status, isRetryable);
|
|
100
|
+
}
|