@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.
Files changed (220) hide show
  1. package/LICENSE +21 -0
  2. package/dist/browser/index.d.ts +1104 -0
  3. package/dist/browser/index.d.ts.map +1 -0
  4. package/dist/browser/index.js +7 -0
  5. package/dist/browser/index.js.map +1 -0
  6. package/dist/loggers/index.cjs +1 -0
  7. package/dist/loggers/index.d.ts +151 -0
  8. package/dist/loggers/index.d.ts.map +1 -0
  9. package/dist/loggers/index.js +2 -0
  10. package/dist/loggers/index.js.map +1 -0
  11. package/dist/node/anthropic/index.cjs +1 -0
  12. package/dist/node/anthropic/index.d.ts +158 -0
  13. package/dist/node/anthropic/index.d.ts.map +1 -0
  14. package/dist/node/anthropic/index.js +1 -0
  15. package/dist/node/anthropic--1vgLC-e.js +5 -0
  16. package/dist/node/anthropic--1vgLC-e.js.map +1 -0
  17. package/dist/node/anthropic-BFQ6DSCP.cjs +4 -0
  18. package/dist/node/bytedance/index.cjs +1 -0
  19. package/dist/node/bytedance/index.d.ts +74 -0
  20. package/dist/node/bytedance/index.d.ts.map +1 -0
  21. package/dist/node/bytedance/index.js +1 -0
  22. package/dist/node/bytedance-C_0sF_pJ.js +2 -0
  23. package/dist/node/bytedance-C_0sF_pJ.js.map +1 -0
  24. package/dist/node/bytedance-DVPxqEiC.cjs +1 -0
  25. package/dist/node/chunk-Bmb41Sf3.cjs +1 -0
  26. package/dist/node/deepseek/index.cjs +1 -0
  27. package/dist/node/deepseek/index.d.ts +2 -0
  28. package/dist/node/deepseek/index.js +1 -0
  29. package/dist/node/deepseek-_8Ixx7rA.js +2 -0
  30. package/dist/node/deepseek-_8Ixx7rA.js.map +1 -0
  31. package/dist/node/deepseek-oA2Y6bD0.cjs +1 -0
  32. package/dist/node/gemini/index.cjs +1 -0
  33. package/dist/node/gemini/index.d.ts +173 -0
  34. package/dist/node/gemini/index.d.ts.map +1 -0
  35. package/dist/node/gemini/index.js +1 -0
  36. package/dist/node/gemini-Bh2U87MY.js +4 -0
  37. package/dist/node/gemini-Bh2U87MY.js.map +1 -0
  38. package/dist/node/gemini-DSaNCxZj.cjs +3 -0
  39. package/dist/node/gemma/index.cjs +1 -0
  40. package/dist/node/gemma/index.d.ts +2 -0
  41. package/dist/node/gemma/index.js +1 -0
  42. package/dist/node/gemma-Dp_AfCUR.js +2 -0
  43. package/dist/node/gemma-Dp_AfCUR.js.map +1 -0
  44. package/dist/node/gemma-G-Pf_PnX.cjs +1 -0
  45. package/dist/node/google/index.cjs +1 -0
  46. package/dist/node/google/index.d.ts +14 -0
  47. package/dist/node/google/index.d.ts.map +1 -0
  48. package/dist/node/google/index.js +2 -0
  49. package/dist/node/google/index.js.map +1 -0
  50. package/dist/node/index-B6PnlDMd.d.ts +82 -0
  51. package/dist/node/index-B6PnlDMd.d.ts.map +1 -0
  52. package/dist/node/index-B7UvPJcI.d.ts +315 -0
  53. package/dist/node/index-B7UvPJcI.d.ts.map +1 -0
  54. package/dist/node/index-BLPOTNb5.d.ts +98 -0
  55. package/dist/node/index-BLPOTNb5.d.ts.map +1 -0
  56. package/dist/node/index-BqixM_XD.d.ts +231 -0
  57. package/dist/node/index-BqixM_XD.d.ts.map +1 -0
  58. package/dist/node/index-C3beaqKO.d.ts +231 -0
  59. package/dist/node/index-C3beaqKO.d.ts.map +1 -0
  60. package/dist/node/index-Cp2XRh9G.d.ts +82 -0
  61. package/dist/node/index-Cp2XRh9G.d.ts.map +1 -0
  62. package/dist/node/index-DSv5xruI.d.ts +98 -0
  63. package/dist/node/index-DSv5xruI.d.ts.map +1 -0
  64. package/dist/node/index-w0bV1uaP.d.ts +315 -0
  65. package/dist/node/index-w0bV1uaP.d.ts.map +1 -0
  66. package/dist/node/index.cjs +1 -0
  67. package/dist/node/index.d.ts +8 -0
  68. package/dist/node/index.js +1 -0
  69. package/dist/node/openai/index.cjs +1 -0
  70. package/dist/node/openai/index.d.ts +2 -0
  71. package/dist/node/openai/index.js +1 -0
  72. package/dist/node/openai-CRQjg4xF.js +2 -0
  73. package/dist/node/openai-CRQjg4xF.js.map +1 -0
  74. package/dist/node/openai-compatible-BYfyY5lb.cjs +1 -0
  75. package/dist/node/openai-compatible-Dm4Sof9e.js +2 -0
  76. package/dist/node/openai-compatible-Dm4Sof9e.js.map +1 -0
  77. package/dist/node/openai-xWC6pY7r.cjs +1 -0
  78. package/dist/node/qwen/index.cjs +1 -0
  79. package/dist/node/qwen/index.d.ts +2 -0
  80. package/dist/node/qwen/index.js +1 -0
  81. package/dist/node/qwen-ChUZobTL.js +2 -0
  82. package/dist/node/qwen-ChUZobTL.js.map +1 -0
  83. package/dist/node/qwen-CjT71vSM.cjs +1 -0
  84. package/package.json +157 -0
  85. package/src/anthropic/__tests__/abort-streaming.test.ts +199 -0
  86. package/src/anthropic/__tests__/model-catalog-refresh.test.ts +92 -0
  87. package/src/anthropic/__tests__/provider-definition.test.ts +55 -0
  88. package/src/anthropic/__tests__/provider.test.ts +1357 -0
  89. package/src/anthropic/__tests__/response-parser.test.ts +326 -0
  90. package/src/anthropic/index.ts +22 -0
  91. package/src/anthropic/message-converter.ts +181 -0
  92. package/src/anthropic/model-catalog-refresh.ts +128 -0
  93. package/src/anthropic/parsers/response-parser.ts +184 -0
  94. package/src/anthropic/provider-definition.ts +93 -0
  95. package/src/anthropic/provider.ts +290 -0
  96. package/src/anthropic/streaming-handler.ts +204 -0
  97. package/src/anthropic/types/api-types.ts +158 -0
  98. package/src/anthropic/types.ts +79 -0
  99. package/src/bytedance/http-client.test.ts +288 -0
  100. package/src/bytedance/http-client.ts +163 -0
  101. package/src/bytedance/index.ts +2 -0
  102. package/src/bytedance/provider.spec.ts +320 -0
  103. package/src/bytedance/provider.ts +171 -0
  104. package/src/bytedance/status-mapper.test.ts +299 -0
  105. package/src/bytedance/status-mapper.ts +141 -0
  106. package/src/bytedance/types.ts +68 -0
  107. package/src/deepseek/defaults.ts +4 -0
  108. package/src/deepseek/index.ts +22 -0
  109. package/src/deepseek/model-catalog-refresh.test.ts +57 -0
  110. package/src/deepseek/model-catalog-refresh.ts +105 -0
  111. package/src/deepseek/model-catalog.ts +55 -0
  112. package/src/deepseek/provider-definition.test.ts +109 -0
  113. package/src/deepseek/provider-definition.ts +132 -0
  114. package/src/deepseek/provider.test.ts +324 -0
  115. package/src/deepseek/provider.ts +298 -0
  116. package/src/deepseek/types.ts +37 -0
  117. package/src/gemini/execution-helpers.ts +233 -0
  118. package/src/gemini/genai-transport.test.ts +208 -0
  119. package/src/gemini/image-operations.test.ts +448 -0
  120. package/src/gemini/image-operations.ts +261 -0
  121. package/src/gemini/index.ts +11 -0
  122. package/src/gemini/message-converter.test.ts +616 -0
  123. package/src/gemini/message-converter.ts +140 -0
  124. package/src/gemini/model-catalog-refresh.test.ts +107 -0
  125. package/src/gemini/model-catalog-refresh.ts +92 -0
  126. package/src/gemini/provider-definition.test.ts +70 -0
  127. package/src/gemini/provider-definition.ts +78 -0
  128. package/src/gemini/provider-extended.test.ts +898 -0
  129. package/src/gemini/provider.spec.ts +216 -0
  130. package/src/gemini/provider.ts +279 -0
  131. package/src/gemini/request-converter.ts +226 -0
  132. package/src/gemini/tool-schema-converter.ts +78 -0
  133. package/src/gemini/types/api-types.ts +235 -0
  134. package/src/gemini/types.ts +121 -0
  135. package/src/gemma/index.ts +5 -0
  136. package/src/gemma/message-factory.ts +38 -0
  137. package/src/gemma/provider-definition.test.ts +43 -0
  138. package/src/gemma/provider-definition.ts +84 -0
  139. package/src/gemma/provider-projection.ts +49 -0
  140. package/src/gemma/provider.test.ts +628 -0
  141. package/src/gemma/provider.ts +308 -0
  142. package/src/gemma/pseudo-command-envelope.ts +58 -0
  143. package/src/gemma/pseudo-tool-call-projector.ts +243 -0
  144. package/src/gemma/pseudo-tool-call-tag-parser.ts +153 -0
  145. package/src/gemma/pseudo-tool-call-types.ts +31 -0
  146. package/src/gemma/reasoning-projector.test.ts +52 -0
  147. package/src/gemma/reasoning-projector.ts +144 -0
  148. package/src/gemma/streaming-projection.ts +79 -0
  149. package/src/gemma/tool-call-argument-parser.ts +126 -0
  150. package/src/gemma/tool-call-projector.test.ts +227 -0
  151. package/src/gemma/tool-call-projector.ts +264 -0
  152. package/src/gemma/types.ts +27 -0
  153. package/src/google/index.ts +11 -0
  154. package/src/google/provider-compat.test.ts +19 -0
  155. package/src/google/provider-definition.ts +6 -0
  156. package/src/google/provider.ts +10 -0
  157. package/src/google/types.ts +5 -0
  158. package/src/index.ts +9 -0
  159. package/src/openai/adapter.test.ts +494 -0
  160. package/src/openai/adapter.ts +145 -0
  161. package/src/openai/chat-completions-chat.ts +189 -0
  162. package/src/openai/executor-integration.test.ts +206 -0
  163. package/src/openai/index.ts +21 -0
  164. package/src/openai/interfaces/payload-logger.ts +48 -0
  165. package/src/openai/loggers/console-payload-logger.test.ts +173 -0
  166. package/src/openai/loggers/console-payload-logger.ts +94 -0
  167. package/src/openai/loggers/console.ts +9 -0
  168. package/src/openai/loggers/file-payload-logger.test.ts +238 -0
  169. package/src/openai/loggers/file-payload-logger.ts +112 -0
  170. package/src/openai/loggers/file.ts +9 -0
  171. package/src/openai/loggers/index.ts +12 -0
  172. package/src/openai/loggers/sanitize-openai-log-data.test.ts +89 -0
  173. package/src/openai/loggers/sanitize-openai-log-data.ts +14 -0
  174. package/src/openai/message-converter.ts +22 -0
  175. package/src/openai/model-catalog-refresh.test.ts +92 -0
  176. package/src/openai/model-catalog-refresh.ts +115 -0
  177. package/src/openai/openai-request-format.ts +92 -0
  178. package/src/openai/parsers/response-parser.test.ts +407 -0
  179. package/src/openai/parsers/response-parser.ts +47 -0
  180. package/src/openai/provider-definition.test.ts +75 -0
  181. package/src/openai/provider-definition.ts +132 -0
  182. package/src/openai/provider.test.ts +1402 -0
  183. package/src/openai/provider.ts +237 -0
  184. package/src/openai/responses-chat.ts +258 -0
  185. package/src/openai/responses-converter.ts +112 -0
  186. package/src/openai/responses-parser.ts +285 -0
  187. package/src/openai/responses-stream-utils.ts +45 -0
  188. package/src/openai/responses-types.ts +195 -0
  189. package/src/openai/streaming/stream-assembler.ts +3 -0
  190. package/src/openai/streaming/stream-handler.test.ts +367 -0
  191. package/src/openai/streaming/stream-handler.ts +119 -0
  192. package/src/openai/types/api-types.ts +112 -0
  193. package/src/openai/types.ts +194 -0
  194. package/src/qwen/defaults.ts +26 -0
  195. package/src/qwen/index.ts +5 -0
  196. package/src/qwen/model-catalog-refresh.test.ts +91 -0
  197. package/src/qwen/model-catalog-refresh.ts +97 -0
  198. package/src/qwen/provider-capabilities.ts +34 -0
  199. package/src/qwen/provider-definition.test.ts +139 -0
  200. package/src/qwen/provider-definition.ts +173 -0
  201. package/src/qwen/provider-streaming-assembly.ts +40 -0
  202. package/src/qwen/provider.test.ts +640 -0
  203. package/src/qwen/provider.ts +293 -0
  204. package/src/qwen/responses-chat.ts +194 -0
  205. package/src/qwen/responses-converter.ts +104 -0
  206. package/src/qwen/responses-parser.ts +299 -0
  207. package/src/qwen/responses-stream-utils.ts +38 -0
  208. package/src/qwen/types.ts +228 -0
  209. package/src/shared/openai-compatible/endpoint-probe.test.ts +52 -0
  210. package/src/shared/openai-compatible/endpoint-probe.ts +43 -0
  211. package/src/shared/openai-compatible/index.ts +6 -0
  212. package/src/shared/openai-compatible/message-converter.test.ts +111 -0
  213. package/src/shared/openai-compatible/message-converter.ts +84 -0
  214. package/src/shared/openai-compatible/native-payload-observer.test.ts +43 -0
  215. package/src/shared/openai-compatible/native-payload-observer.ts +26 -0
  216. package/src/shared/openai-compatible/response-parser.test.ts +172 -0
  217. package/src/shared/openai-compatible/response-parser.ts +180 -0
  218. package/src/shared/openai-compatible/stream-assembler.test.ts +266 -0
  219. package/src/shared/openai-compatible/stream-assembler.ts +248 -0
  220. package/src/shared/openai-compatible/types.ts +59 -0
@@ -0,0 +1,140 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { Part, FunctionCall, GenerateContentResponse } from '@google/genai';
3
+ import type {
4
+ TUniversalMessage,
5
+ IAssistantMessage,
6
+ TUniversalMessagePart,
7
+ } from '@robota-sdk/agent-core';
8
+
9
+ const RANDOM_ID_RADIX = 36;
10
+ const RANDOM_ID_LENGTH = 9;
11
+
12
+ interface ICollectedGeminiParts {
13
+ textValues: string[];
14
+ messageParts: TUniversalMessagePart[];
15
+ functionCalls: FunctionCall[];
16
+ }
17
+
18
+ export {
19
+ convertToGeminiFormat,
20
+ convertToGeminiFormatWithInlineSystem,
21
+ convertToGeminiRequestFormat,
22
+ mapMessagePartsToGeminiParts,
23
+ } from './request-converter';
24
+ export type { IGeminiMessageConversionResult } from './request-converter';
25
+
26
+ /** Generates a unique call identifier for function call responses. */
27
+ export function generateCallId(): string {
28
+ return `call_${Date.now()}_${Math.random().toString(RANDOM_ID_RADIX).substr(2, RANDOM_ID_LENGTH)}`;
29
+ }
30
+
31
+ /** Converts a Gemini API response into a universal message. */
32
+ export function convertFromGeminiResponse(response: GenerateContentResponse): TUniversalMessage {
33
+ const candidate = response.candidates?.[0];
34
+ if (!candidate) {
35
+ throw new Error('No candidate in Gemini response');
36
+ }
37
+
38
+ const content = candidate.content;
39
+ if (!content || !content.parts || content.parts.length === 0) {
40
+ throw new Error('No content in Gemini response');
41
+ }
42
+
43
+ const collectedParts = collectGeminiParts(content.parts);
44
+
45
+ const result: TUniversalMessage = {
46
+ id: randomUUID(),
47
+ state: 'complete' as const,
48
+ role: 'assistant',
49
+ content: collectedParts.textValues.length > 0 ? collectedParts.textValues.join('') : null,
50
+ parts: collectedParts.messageParts,
51
+ timestamp: new Date(),
52
+ };
53
+
54
+ if (collectedParts.functionCalls.length > 0) {
55
+ const assistantResult = result as IAssistantMessage;
56
+ assistantResult.toolCalls = collectedParts.functionCalls.map((fc) => ({
57
+ id: fc.id ?? generateCallId(),
58
+ type: 'function' as const,
59
+ function: {
60
+ name: requireFunctionCallName(fc),
61
+ arguments: JSON.stringify(fc.args ?? {}),
62
+ },
63
+ }));
64
+ }
65
+
66
+ const usageMetadata = mapUsageMetadata(response);
67
+ if (usageMetadata) {
68
+ result.metadata = usageMetadata;
69
+ }
70
+
71
+ return result;
72
+ }
73
+
74
+ function collectGeminiParts(parts: Part[]): ICollectedGeminiParts {
75
+ const textValues: string[] = [];
76
+ const messageParts: TUniversalMessagePart[] = [];
77
+ const functionCalls: FunctionCall[] = [];
78
+
79
+ for (const part of parts) {
80
+ collectTextPart(part, textValues, messageParts);
81
+ collectInlineImagePart(part, messageParts);
82
+ if (part.functionCall) {
83
+ functionCalls.push(part.functionCall);
84
+ }
85
+ }
86
+
87
+ return { textValues, messageParts, functionCalls };
88
+ }
89
+
90
+ function collectTextPart(
91
+ part: Part,
92
+ textValues: string[],
93
+ messageParts: TUniversalMessagePart[],
94
+ ): void {
95
+ if (typeof part.text !== 'string') {
96
+ return;
97
+ }
98
+ textValues.push(part.text);
99
+ messageParts.push({ type: 'text', text: part.text });
100
+ }
101
+
102
+ function collectInlineImagePart(part: Part, messageParts: TUniversalMessagePart[]): void {
103
+ if (
104
+ !part.inlineData ||
105
+ typeof part.inlineData.data !== 'string' ||
106
+ typeof part.inlineData.mimeType !== 'string'
107
+ ) {
108
+ return;
109
+ }
110
+ messageParts.push({
111
+ type: 'image_inline',
112
+ data: part.inlineData.data,
113
+ mimeType: part.inlineData.mimeType,
114
+ });
115
+ }
116
+
117
+ function mapUsageMetadata(response: GenerateContentResponse): TUniversalMessage['metadata'] {
118
+ if (
119
+ !response.usageMetadata ||
120
+ typeof response.usageMetadata.promptTokenCount !== 'number' ||
121
+ typeof response.usageMetadata.candidatesTokenCount !== 'number' ||
122
+ typeof response.usageMetadata.totalTokenCount !== 'number'
123
+ ) {
124
+ return undefined;
125
+ }
126
+ return {
127
+ promptTokens: response.usageMetadata.promptTokenCount,
128
+ completionTokens: response.usageMetadata.candidatesTokenCount,
129
+ totalTokens: response.usageMetadata.totalTokenCount,
130
+ };
131
+ }
132
+
133
+ export { convertToolsToGeminiFormat } from './tool-schema-converter';
134
+
135
+ function requireFunctionCallName(functionCall: FunctionCall): string {
136
+ if (!functionCall.name || functionCall.name.trim().length === 0) {
137
+ throw new Error('Gemini function call is missing a function name.');
138
+ }
139
+ return functionCall.name;
140
+ }
@@ -0,0 +1,107 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { refreshGeminiModelCatalog } from './model-catalog-refresh';
3
+
4
+ describe('refreshGeminiModelCatalog', () => {
5
+ it('maps the live Gemini /models response into provider model catalog entries, filtering non-generateContent models', async () => {
6
+ const fetcher = vi.fn(async () => ({
7
+ ok: true,
8
+ status: 200,
9
+ json: async () => ({
10
+ models: [
11
+ {
12
+ name: 'models/gemini-2.0-flash',
13
+ displayName: 'Gemini 2.0 Flash',
14
+ description: 'Fast model',
15
+ inputTokenLimit: 1048576,
16
+ outputTokenLimit: 8192,
17
+ supportedGenerationMethods: ['generateContent', 'streamGenerateContent'],
18
+ },
19
+ {
20
+ name: 'models/gemini-2.0-pro',
21
+ displayName: 'Gemini 2.0 Pro',
22
+ description: 'Pro model',
23
+ inputTokenLimit: 1048576,
24
+ outputTokenLimit: 8192,
25
+ supportedGenerationMethods: ['generateContent'],
26
+ },
27
+ {
28
+ name: 'models/embedding-001',
29
+ displayName: 'Embedding 001',
30
+ description: 'Embedding model only',
31
+ inputTokenLimit: 2048,
32
+ outputTokenLimit: 1,
33
+ supportedGenerationMethods: ['embedContent'],
34
+ },
35
+ ],
36
+ }),
37
+ }));
38
+
39
+ const catalog = await refreshGeminiModelCatalog({ apiKey: 'gemini-key' }, fetcher);
40
+
41
+ expect(fetcher).toHaveBeenCalledWith(
42
+ 'https://generativelanguage.googleapis.com/v1beta/models?key=gemini-key',
43
+ );
44
+ expect(catalog.status).toBe('live');
45
+ expect(catalog.entries?.map((entry) => entry.id)).toEqual([
46
+ 'gemini-2.0-flash',
47
+ 'gemini-2.0-pro',
48
+ ]);
49
+ expect(catalog.entries?.[0]).toMatchObject({
50
+ id: 'gemini-2.0-flash',
51
+ displayName: 'Gemini 2.0 Flash',
52
+ lifecycle: 'active',
53
+ });
54
+ });
55
+
56
+ it('sets lifecycle to preview when model name contains "preview"', async () => {
57
+ const fetcher = vi.fn(async () => ({
58
+ ok: true,
59
+ status: 200,
60
+ json: async () => ({
61
+ models: [
62
+ {
63
+ name: 'models/gemini-3-flash-preview',
64
+ displayName: 'Gemini 3 Flash Preview',
65
+ supportedGenerationMethods: ['generateContent', 'streamGenerateContent'],
66
+ },
67
+ ],
68
+ }),
69
+ }));
70
+
71
+ const catalog = await refreshGeminiModelCatalog({}, fetcher);
72
+
73
+ expect(catalog.status).toBe('live');
74
+ expect(catalog.entries?.[0]).toMatchObject({
75
+ id: 'gemini-3-flash-preview',
76
+ displayName: 'Gemini 3 Flash Preview',
77
+ lifecycle: 'preview',
78
+ });
79
+ });
80
+
81
+ it('returns an unavailable catalog when the live model refresh fails', async () => {
82
+ const fetcher = vi.fn(async () => ({
83
+ ok: false,
84
+ status: 403,
85
+ json: async () => ({ models: [] }),
86
+ }));
87
+
88
+ const catalog = await refreshGeminiModelCatalog({}, fetcher);
89
+
90
+ expect(catalog).toMatchObject({
91
+ status: 'unavailable',
92
+ message: 'Gemini model refresh failed: HTTP 403',
93
+ });
94
+ });
95
+
96
+ it('builds URL without key param when no apiKey is provided', async () => {
97
+ const fetcher = vi.fn(async () => ({
98
+ ok: true,
99
+ status: 200,
100
+ json: async () => ({ models: [] }),
101
+ }));
102
+
103
+ await refreshGeminiModelCatalog({}, fetcher);
104
+
105
+ expect(fetcher).toHaveBeenCalledWith('https://generativelanguage.googleapis.com/v1beta/models');
106
+ });
107
+ });
@@ -0,0 +1,92 @@
1
+ import type {
2
+ IProviderModelCatalog,
3
+ IProviderModelCatalogEntry,
4
+ IProviderProfileConfig,
5
+ } from '@robota-sdk/agent-core';
6
+ import { GEMINI_MODEL_LAST_VERIFIED_AT, GEMINI_MODEL_SOURCE_URL } from './provider-definition';
7
+
8
+ const GEMINI_API_ENDPOINT = 'https://generativelanguage.googleapis.com/v1beta/models';
9
+
10
+ export interface IGeminiModelInfo {
11
+ name?: string;
12
+ displayName?: string;
13
+ description?: string;
14
+ inputTokenLimit?: number;
15
+ outputTokenLimit?: number;
16
+ supportedGenerationMethods?: string[];
17
+ }
18
+
19
+ export interface IGeminiModelsResponse {
20
+ models?: IGeminiModelInfo[];
21
+ nextPageToken?: string;
22
+ }
23
+
24
+ export interface IGeminiFetchInit {
25
+ headers?: Record<string, string>;
26
+ }
27
+
28
+ export interface IGeminiFetchResponse {
29
+ ok: boolean;
30
+ status: number;
31
+ json: () => Promise<IGeminiModelsResponse>;
32
+ }
33
+
34
+ export type TGeminiFetch = (url: string, init?: IGeminiFetchInit) => Promise<IGeminiFetchResponse>;
35
+
36
+ export async function refreshGeminiModelCatalog(
37
+ profile: IProviderProfileConfig,
38
+ fetcher: TGeminiFetch = defaultGeminiFetch,
39
+ ): Promise<IProviderModelCatalog> {
40
+ const url = profile.apiKey ? `${GEMINI_API_ENDPOINT}?key=${profile.apiKey}` : GEMINI_API_ENDPOINT;
41
+
42
+ const response = await fetcher(url);
43
+
44
+ if (!response.ok) {
45
+ return {
46
+ status: 'unavailable',
47
+ sourceUrl: GEMINI_MODEL_SOURCE_URL,
48
+ message: `Gemini model refresh failed: HTTP ${response.status}`,
49
+ };
50
+ }
51
+
52
+ const body = await response.json();
53
+ const entries = (body.models ?? []).filter(supportsGenerateContent).map(toModelCatalogEntry);
54
+
55
+ return {
56
+ status: 'live',
57
+ sourceUrl: GEMINI_MODEL_SOURCE_URL,
58
+ lastVerifiedAt: new Date().toISOString(),
59
+ entries,
60
+ message: `Fetched ${entries.length} Gemini models`,
61
+ };
62
+ }
63
+
64
+ function supportsGenerateContent(model: IGeminiModelInfo): boolean {
65
+ return model.supportedGenerationMethods?.includes('generateContent') === true;
66
+ }
67
+
68
+ function toModelCatalogEntry(model: IGeminiModelInfo): IProviderModelCatalogEntry {
69
+ const rawName = model.name ?? '';
70
+ const id = rawName.startsWith('models/') ? rawName.slice('models/'.length) : rawName;
71
+ const lifecycle = rawName.includes('preview') ? 'preview' : 'active';
72
+
73
+ return {
74
+ id,
75
+ displayName: model.displayName ?? id,
76
+ lifecycle,
77
+ sourceUrl: GEMINI_MODEL_SOURCE_URL,
78
+ lastVerifiedAt: GEMINI_MODEL_LAST_VERIFIED_AT,
79
+ };
80
+ }
81
+
82
+ async function defaultGeminiFetch(
83
+ url: string,
84
+ _init?: IGeminiFetchInit,
85
+ ): Promise<IGeminiFetchResponse> {
86
+ const response = await fetch(url);
87
+ return {
88
+ ok: response.ok,
89
+ status: response.status,
90
+ json: () => response.json() as Promise<IGeminiModelsResponse>,
91
+ };
92
+ }
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { GeminiProvider } from './provider';
3
+ import {
4
+ DEFAULT_GEMINI_PROVIDER_API_KEY_REFERENCE,
5
+ DEFAULT_GEMINI_PROVIDER_MODEL,
6
+ createGeminiProviderDefinition,
7
+ } from './provider-definition';
8
+
9
+ vi.mock('@google/genai', () => {
10
+ class GoogleGenAI {
11
+ public constructor(_options: { apiKey: string }) {}
12
+ }
13
+ return {
14
+ GoogleGenAI,
15
+ Type: {
16
+ STRING: 'STRING',
17
+ NUMBER: 'NUMBER',
18
+ INTEGER: 'INTEGER',
19
+ BOOLEAN: 'BOOLEAN',
20
+ ARRAY: 'ARRAY',
21
+ OBJECT: 'OBJECT',
22
+ },
23
+ };
24
+ });
25
+
26
+ describe('createGeminiProviderDefinition', () => {
27
+ it('exposes Gemini as the canonical provider type with Google compatibility alias', () => {
28
+ const definition = createGeminiProviderDefinition();
29
+
30
+ expect(definition.type).toBe('gemini');
31
+ expect(definition.aliases).toEqual(['google']);
32
+ expect(definition.displayName).toBe('Gemini');
33
+ expect(definition.defaults).toEqual({
34
+ model: DEFAULT_GEMINI_PROVIDER_MODEL,
35
+ apiKey: DEFAULT_GEMINI_PROVIDER_API_KEY_REFERENCE,
36
+ });
37
+ expect(definition.setupHelpLinks).toEqual([
38
+ {
39
+ kind: 'api-key',
40
+ label: 'Google AI Studio API keys',
41
+ url: 'https://aistudio.google.com/apikey',
42
+ sourceUrl: 'https://ai.google.dev/gemini-api/docs/api-key',
43
+ lastVerifiedAt: '2026-05-08',
44
+ },
45
+ ]);
46
+ });
47
+
48
+ it('creates the Gemini API provider implementation from generic provider config', () => {
49
+ const definition = createGeminiProviderDefinition();
50
+
51
+ const provider = definition.createProvider({
52
+ name: 'gemini',
53
+ model: 'gemini-3-flash-preview',
54
+ apiKey: 'gemini-key',
55
+ });
56
+
57
+ expect(provider).toBeInstanceOf(GeminiProvider);
58
+ });
59
+
60
+ it('fails clearly when apiKey is missing', () => {
61
+ const definition = createGeminiProviderDefinition();
62
+
63
+ expect(() =>
64
+ definition.createProvider({
65
+ name: 'gemini',
66
+ model: 'gemini-3-flash-preview',
67
+ }),
68
+ ).toThrow('Provider gemini requires apiKey');
69
+ });
70
+ });
@@ -0,0 +1,78 @@
1
+ import type { IProviderDefinition } from '@robota-sdk/agent-core';
2
+ import { refreshGeminiModelCatalog } from './model-catalog-refresh';
3
+ import { GeminiProvider } from './provider';
4
+
5
+ export const DEFAULT_GEMINI_PROVIDER_API_KEY_ENV = 'GEMINI_API_KEY';
6
+ export const DEFAULT_GEMINI_PROVIDER_API_KEY_REFERENCE = `$ENV:${DEFAULT_GEMINI_PROVIDER_API_KEY_ENV}`;
7
+ export const DEFAULT_GEMINI_PROVIDER_MODEL = 'gemini-3-flash-preview';
8
+ export const GEMINI_MODEL_SOURCE_URL = 'https://ai.google.dev/api/models';
9
+ export const GEMINI_MODEL_LAST_VERIFIED_AT = '2026-05-04';
10
+ const GEMINI_API_KEY_URL = 'https://aistudio.google.com/apikey';
11
+ const GEMINI_SETUP_SOURCE_URL = 'https://ai.google.dev/gemini-api/docs/api-key';
12
+ const GEMINI_SETUP_LAST_VERIFIED_AT = '2026-05-08';
13
+ const GEMINI_SETUP_HELP_LINKS: NonNullable<IProviderDefinition['setupHelpLinks']> = [
14
+ {
15
+ kind: 'api-key',
16
+ label: 'Google AI Studio API keys',
17
+ url: GEMINI_API_KEY_URL,
18
+ sourceUrl: GEMINI_SETUP_SOURCE_URL,
19
+ lastVerifiedAt: GEMINI_SETUP_LAST_VERIFIED_AT,
20
+ },
21
+ ];
22
+
23
+ export function createGeminiProviderDefinition(): IProviderDefinition {
24
+ return {
25
+ type: 'gemini',
26
+ aliases: ['google'],
27
+ displayName: 'Gemini',
28
+ description: 'Google Gemini API provider',
29
+ defaults: {
30
+ model: DEFAULT_GEMINI_PROVIDER_MODEL,
31
+ apiKey: DEFAULT_GEMINI_PROVIDER_API_KEY_REFERENCE,
32
+ },
33
+ modelCatalog: {
34
+ status: 'fallback',
35
+ sourceUrl: GEMINI_MODEL_SOURCE_URL,
36
+ lastVerifiedAt: GEMINI_MODEL_LAST_VERIFIED_AT,
37
+ entries: [
38
+ {
39
+ id: DEFAULT_GEMINI_PROVIDER_MODEL,
40
+ displayName: 'Gemini 3 Flash Preview',
41
+ capabilities: ['tools', 'vision', 'json_schema', 'reasoning', 'streaming'],
42
+ lifecycle: 'preview',
43
+ sourceUrl: GEMINI_MODEL_SOURCE_URL,
44
+ lastVerifiedAt: GEMINI_MODEL_LAST_VERIFIED_AT,
45
+ },
46
+ ],
47
+ },
48
+ setupHelpLinks: GEMINI_SETUP_HELP_LINKS,
49
+ setupSteps: [
50
+ {
51
+ key: 'model',
52
+ title: 'Gemini model',
53
+ defaultValue: DEFAULT_GEMINI_PROVIDER_MODEL,
54
+ },
55
+ {
56
+ key: 'apiKey',
57
+ title: 'Gemini API key',
58
+ defaultValue: DEFAULT_GEMINI_PROVIDER_API_KEY_REFERENCE,
59
+ masked: true,
60
+ },
61
+ ],
62
+ requiresApiKey: true,
63
+ refreshModelCatalog: ({ profile }) => refreshGeminiModelCatalog(profile),
64
+ modelCatalogCacheTtlSeconds: 86400,
65
+ createProvider: (config) =>
66
+ new GeminiProvider({
67
+ apiKey: requireApiKey(config.apiKey),
68
+ defaultModel: config.model,
69
+ }),
70
+ };
71
+ }
72
+
73
+ function requireApiKey(apiKey: string | undefined): string {
74
+ if (!apiKey) {
75
+ throw new Error('Provider gemini requires apiKey');
76
+ }
77
+ return apiKey;
78
+ }