@opencode-ai/ai 0.0.0-next-16996 → 0.0.0-next-16997

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.
@@ -83,6 +83,9 @@ declare const GeminiBody: Schema.Struct<{
83
83
  readonly temperature: Schema.optional<Schema.Number>;
84
84
  readonly topP: Schema.optional<Schema.Number>;
85
85
  readonly topK: Schema.optional<Schema.Number>;
86
+ readonly frequencyPenalty: Schema.optional<Schema.Number>;
87
+ readonly presencePenalty: Schema.optional<Schema.Number>;
88
+ readonly seed: Schema.optional<Schema.Number>;
86
89
  readonly stopSequences: Schema.optional<Schema.$Array<Schema.String>>;
87
90
  readonly thinkingConfig: Schema.optional<Schema.Struct<{
88
91
  readonly thinkingBudget: Schema.optional<Schema.Number>;
@@ -160,6 +163,9 @@ export declare const protocol: Protocol<{
160
163
  readonly temperature?: number | undefined;
161
164
  readonly topP?: number | undefined;
162
165
  readonly topK?: number | undefined;
166
+ readonly frequencyPenalty?: number | undefined;
167
+ readonly presencePenalty?: number | undefined;
168
+ readonly seed?: number | undefined;
163
169
  readonly stopSequences?: readonly string[] | undefined;
164
170
  readonly thinkingConfig?: {
165
171
  readonly thinkingBudget?: number | undefined;
@@ -279,6 +285,9 @@ export declare const route: Route<{
279
285
  readonly temperature?: number | undefined;
280
286
  readonly topP?: number | undefined;
281
287
  readonly topK?: number | undefined;
288
+ readonly frequencyPenalty?: number | undefined;
289
+ readonly presencePenalty?: number | undefined;
290
+ readonly seed?: number | undefined;
282
291
  readonly stopSequences?: readonly string[] | undefined;
283
292
  readonly thinkingConfig?: {
284
293
  readonly thinkingBudget?: number | undefined;
@@ -12,7 +12,21 @@ import { Lifecycle } from "./utils/lifecycle";
12
12
  import { ToolSchemaProjection } from "./utils/tool-schema";
13
13
  const ADAPTER = "gemini";
14
14
  const MEDIA_MIMES = new Set(ProviderShared.MEDIA_MIMES);
15
+ // Google documents this sentinel for replaying Gemini 3 function calls after their original signature was lost.
16
+ const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator";
15
17
  export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
18
+ // Gemini 3 rejects replayed function calls without a thought signature. Google's SDKs avoid that in normal chats by
19
+ // retaining complete model responses, but OpenCode reconstructs durable history and may encounter an unsigned call
20
+ // from an older or external session. Model IDs are open-ended, so unknown Gemini aliases inherit current behavior.
21
+ const requiresThoughtSignatureFallback = (modelID) => {
22
+ if (!/(^|\/)gemini-/i.test(modelID))
23
+ return false;
24
+ if (/(^|\/)gemini-(?:1|2)(?:[.-]|$)/i.test(modelID))
25
+ return false;
26
+ if (/(^|\/)gemini-pro(?:-vision)?$/i.test(modelID))
27
+ return false;
28
+ return !/(^|\/)gemini-robotics-er-1\.5(?:[.-]|$)/i.test(modelID);
29
+ };
16
30
  // =============================================================================
17
31
  // Request Body Schema
18
32
  // =============================================================================
@@ -84,6 +98,9 @@ const GeminiGenerationConfig = Schema.Struct({
84
98
  temperature: Schema.optional(Schema.Number),
85
99
  topP: Schema.optional(Schema.Number),
86
100
  topK: Schema.optional(Schema.Number),
101
+ frequencyPenalty: Schema.optional(Schema.Number),
102
+ presencePenalty: Schema.optional(Schema.Number),
103
+ seed: Schema.optional(Schema.Number),
87
104
  stopSequences: optionalArray(Schema.String),
88
105
  thinkingConfig: Schema.optional(GeminiThinkingConfig),
89
106
  });
@@ -124,11 +141,13 @@ const GeminiEvent = Schema.Struct({
124
141
  // keys on non-object scalars. Mirrors OpenCode's historical Gemini rules.
125
142
  //
126
143
  // 2. Project — lossy mapping from JSON Schema to Gemini's schema dialect:
127
- // drop empty objects, derive `nullable: true` from `type: [..., "null"]`,
128
- // coerce `const` to `[const]` enum, recurse properties/items, propagate
144
+ // drop empty root parameter schemas while preserving nested empty objects,
145
+ // expand type arrays into `anyOf`, derive `nullable: true` from null members,
146
+ // coerce `const` to `[const]` enum, recurse properties/items, and propagate
129
147
  // only an allowlisted set of keys (description, required, format, type,
130
- // properties, items, allOf, anyOf, oneOf, minLength). Anything outside the
131
- // allowlist (e.g. `additionalProperties`, `$ref`) is silently dropped.
148
+ // nullable, enum, properties, items, allOf, anyOf, oneOf, minLength).
149
+ // Anything outside the allowlist (e.g. `additionalProperties`, `$ref`) is
150
+ // silently dropped.
132
151
  //
133
152
  // Sanitize runs first, then project. The implementation lives in
134
153
  // `utils/gemini-tool-schema` so this protocol keeps the same shape as the other
@@ -194,6 +213,8 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
194
213
  }
195
214
  if (message.role === "assistant") {
196
215
  const parts = [];
216
+ // Parallel Gemini 3 calls may carry one signature on the first call; unsigned sibling calls are valid.
217
+ let hasSignedToolCall = false;
197
218
  for (const part of message.content) {
198
219
  if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"]))
199
220
  return yield* ProviderShared.unsupportedContent("Gemini", "assistant", ["text", "reasoning", "tool-call"]);
@@ -206,7 +227,17 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request) {
206
227
  continue;
207
228
  }
208
229
  if (part.type === "tool-call") {
209
- parts.push(lowerToolCall(part));
230
+ const lowered = lowerToolCall(part);
231
+ const signature = lowered.thoughtSignature;
232
+ parts.push({
233
+ ...lowered,
234
+ thoughtSignature: signature ??
235
+ (requiresThoughtSignatureFallback(request.model.id) && !hasSignedToolCall
236
+ ? SKIP_THOUGHT_SIGNATURE_VALIDATOR
237
+ : undefined),
238
+ });
239
+ if (signature !== undefined)
240
+ hasSignedToolCall = true;
210
241
  continue;
211
242
  }
212
243
  }
@@ -292,6 +323,9 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request) {
292
323
  temperature: generation?.temperature,
293
324
  topP: generation?.topP,
294
325
  topK: generation?.topK,
326
+ frequencyPenalty: generation?.frequencyPenalty,
327
+ presencePenalty: generation?.presencePenalty,
328
+ seed: generation?.seed,
295
329
  stopSequences: generation?.stop,
296
330
  thinkingConfig: options.thinkingConfig,
297
331
  };
@@ -47,37 +47,57 @@ const sanitizeNode = (schema) => {
47
47
  const emptyObjectSchema = (schema) => schema.type === "object" &&
48
48
  (!isRecord(schema.properties) || Object.keys(schema.properties).length === 0) &&
49
49
  !schema.additionalProperties;
50
- const projectNode = (schema) => {
50
+ const projectNode = (schema, nested = false) => {
51
51
  if (!isRecord(schema))
52
52
  return undefined;
53
- if (emptyObjectSchema(schema))
53
+ if (!nested && emptyObjectSchema(schema))
54
54
  return undefined;
55
- return Object.fromEntries([
55
+ const types = Array.isArray(schema.type) ? schema.type.filter((type) => type !== "null") : undefined;
56
+ const anyOf = Array.isArray(schema.anyOf) ? schema.anyOf : undefined;
57
+ const hasNullAnyOf = anyOf?.some((item) => isRecord(item) && item.type === "null") ?? false;
58
+ const anyOfTypes = hasNullAnyOf ? anyOf?.filter((item) => !isRecord(item) || item.type !== "null") : anyOf;
59
+ const flattenedAnyOf = hasNullAnyOf && anyOfTypes?.length === 1 ? projectNode(anyOfTypes[0], true) : undefined;
60
+ const result = Object.fromEntries([
56
61
  ["description", schema.description],
57
62
  ["required", schema.required],
58
63
  ["format", schema.format],
59
- ["type", Array.isArray(schema.type) ? schema.type.filter((type) => type !== "null")[0] : schema.type],
60
- ["nullable", Array.isArray(schema.type) && schema.type.includes("null") ? true : undefined],
64
+ ["type", types ? (types.length === 0 ? "null" : undefined) : schema.type],
65
+ [
66
+ "nullable",
67
+ (Array.isArray(schema.type) && schema.type.includes("null") && types && types.length > 0) || hasNullAnyOf
68
+ ? true
69
+ : undefined,
70
+ ],
61
71
  ["enum", schema.const !== undefined ? [schema.const] : schema.enum],
62
72
  [
63
73
  "properties",
64
74
  isRecord(schema.properties)
65
- ? Object.fromEntries(Object.entries(schema.properties).map(([key, value]) => [key, projectNode(value)]))
75
+ ? Object.fromEntries(Object.entries(schema.properties).map(([key, value]) => [key, projectNode(value, true)]))
66
76
  : undefined,
67
77
  ],
68
78
  [
69
79
  "items",
70
80
  Array.isArray(schema.items)
71
- ? schema.items.map(projectNode)
81
+ ? schema.items.map((item) => projectNode(item, true))
72
82
  : schema.items === undefined
73
83
  ? undefined
74
- : projectNode(schema.items),
84
+ : projectNode(schema.items, true),
85
+ ],
86
+ ["allOf", Array.isArray(schema.allOf) ? schema.allOf.map((item) => projectNode(item, true)) : undefined],
87
+ [
88
+ "anyOf",
89
+ anyOfTypes
90
+ ? hasNullAnyOf && anyOfTypes.length === 1
91
+ ? undefined
92
+ : anyOfTypes.map((item) => projectNode(item, true))
93
+ : types && types.length > 0
94
+ ? types.map((type) => ({ type }))
95
+ : undefined,
75
96
  ],
76
- ["allOf", Array.isArray(schema.allOf) ? schema.allOf.map(projectNode) : undefined],
77
- ["anyOf", Array.isArray(schema.anyOf) ? schema.anyOf.map(projectNode) : undefined],
78
- ["oneOf", Array.isArray(schema.oneOf) ? schema.oneOf.map(projectNode) : undefined],
97
+ ["oneOf", Array.isArray(schema.oneOf) ? schema.oneOf.map((item) => projectNode(item, true)) : undefined],
79
98
  ["minLength", schema.minLength],
80
99
  ].filter((entry) => entry[1] !== undefined));
100
+ return flattenedAnyOf ? { ...result, ...flattenedAnyOf } : result;
81
101
  };
82
102
  export const convert = (schema) => projectNode(sanitizeNode(schema));
83
103
  export * as GeminiToolSchema from "./gemini-tool-schema";
@@ -87,6 +87,9 @@ export declare const routes: Route<{
87
87
  readonly temperature?: number | undefined;
88
88
  readonly topP?: number | undefined;
89
89
  readonly topK?: number | undefined;
90
+ readonly frequencyPenalty?: number | undefined;
91
+ readonly presencePenalty?: number | undefined;
92
+ readonly seed?: number | undefined;
90
93
  readonly stopSequences?: readonly string[] | undefined;
91
94
  readonly thinkingConfig?: {
92
95
  readonly thinkingBudget?: number | undefined;
@@ -70,6 +70,9 @@ export declare const routes: import("../route").Route<{
70
70
  readonly temperature?: number | undefined;
71
71
  readonly topP?: number | undefined;
72
72
  readonly topK?: number | undefined;
73
+ readonly frequencyPenalty?: number | undefined;
74
+ readonly presencePenalty?: number | undefined;
75
+ readonly seed?: number | undefined;
73
76
  readonly stopSequences?: readonly string[] | undefined;
74
77
  readonly thinkingConfig?: {
75
78
  readonly thinkingBudget?: number | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "0.0.0-next-16996",
3
+ "version": "0.0.0-next-16997",
4
4
  "name": "@opencode-ai/ai",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  "devDependencies": {
31
31
  "@clack/prompts": "1.0.0-alpha.1",
32
32
  "@effect/platform-node": "4.0.0-beta.101",
33
- "@opencode-ai/http-recorder": "0.0.0-next-16996",
33
+ "@opencode-ai/http-recorder": "0.0.0-next-16997",
34
34
  "@tsconfig/bun": "1.0.9",
35
35
  "@types/bun": "1.3.13",
36
36
  "@typescript/native-preview": "7.0.0-dev.20251207.1",
@@ -39,7 +39,7 @@
39
39
  "dependencies": {
40
40
  "@smithy/eventstream-codec": "4.2.14",
41
41
  "@smithy/util-utf8": "4.2.2",
42
- "@opencode-ai/schema": "0.0.0-next-16996",
42
+ "@opencode-ai/schema": "0.0.0-next-16997",
43
43
  "aws4fetch": "1.0.20",
44
44
  "effect": "4.0.0-beta.101",
45
45
  "google-auth-library": "10.5.0"