pi-sap-aicore 0.3.2 → 0.3.4

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/CHANGELOG.md CHANGED
@@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.4] - 2026-07-01
11
+
12
+ ### Fixed
13
+
14
+ - Direct GCP Vertex AI (Gemini) foundation route now sends pi tool schemas via
15
+ Gemini's `parametersJsonSchema` (full JSON Schema) instead of the legacy
16
+ `parameters` field (a restricted OpenAPI 3.0 subset). The legacy field rejected
17
+ common JSON Schema constructs with HTTP 400 — `const` ("Unknown name const")
18
+ and non-string `enum` values ("enum[0] (TYPE_STRING)") — which broke Gemini
19
+ foundation models (e.g. `gemini-3.5-flash`) on tool-heavy coding-agent turns.
20
+ - SAP AI Core orchestration route no longer sends `reasoning_effort` alongside
21
+ function tools for `gpt-*` models. SAP's `/v1/chat/completions` (the only
22
+ endpoint reachable through the orchestration SDK) rejects that combination for
23
+ gpt-5.x with a 400 pointing to `/v1/responses`, which is a foundation-models
24
+ deployment endpoint the orchestration SDK cannot target. `reasoning_effort` is
25
+ still sent on text-only turns; SAP continues to spend reasoning tokens
26
+ internally on tool turns (governed by `max_completion_tokens`).
27
+
28
+ ### Added
29
+
30
+ - Added `npm test` (`scripts/test-vertex-tool-schema.mjs`), a fast,
31
+ credential-free regression test asserting the Vertex/Gemini adapter emits
32
+ `parametersJsonSchema` and preserves `const`/boolean-`enum` constructs. Wired
33
+ into `prepublishOnly`. The live `validate:foundation` matrix also gained a
34
+ complex-tool-schema scenario that exercises the real HTTP tool path.
35
+
36
+ ## [0.3.3] - 2026-07-01
37
+
38
+ ### Added
39
+
40
+ - Added `npm run validate:foundation`, a live SAP AI Core validation matrix for
41
+ direct foundation executables. It verifies text generation, real tool execution
42
+ side effects, and image input across Azure OpenAI, AWS Bedrock, and GCP Vertex
43
+ AI foundation routes.
44
+
45
+ ### Changed
46
+
47
+ - Raised SAP AI SDK dependency floors to `^2.12.0` so fresh installs prefer the
48
+ patched transitive `axios` / `form-data` tree that resolves the npm audit
49
+ advisory for `form-data <4.0.6`.
50
+
10
51
  ## [0.3.2] - 2026-07-01
11
52
 
12
53
  ### Fixed
@@ -146,7 +187,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
146
187
  `reasoning_effort` for OpenAI).
147
188
  - MIT license and npm packaging.
148
189
 
149
- [Unreleased]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.2...HEAD
190
+ [Unreleased]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.3...HEAD
191
+ [0.3.3]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.2...v0.3.3
150
192
  [0.3.2]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.1...v0.3.2
151
193
  [0.3.1]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.3.0...v0.3.1
152
194
  [0.3.0]: https://github.com/ttiimmaahh/pi-sap-aicore/compare/v0.2.2...v0.3.0
package/README.md CHANGED
@@ -160,6 +160,26 @@ there. An id with no matching deployment 404s at call time. Run
160
160
  `/sap-models discover` in pi (or `node scripts/list-sap-models.mjs` from this
161
161
  repo) to see what your tenant actually deploys.
162
162
 
163
+ ### Live foundation validation
164
+
165
+ To validate normal coding-agent behavior across the three direct foundation
166
+ executables, run the live smoke-test matrix from this repo:
167
+
168
+ ```bash
169
+ npm run validate:foundation
170
+ ```
171
+
172
+ The script uses `pi --no-extensions -e ./index.ts` so it tests the local checkout,
173
+ not a globally installed package. It makes real SAP model calls and validates:
174
+
175
+ - text generation
176
+ - real tool execution via side-effect files
177
+ - image input smoke tests
178
+
179
+ Default models are `gpt-5.5`, `anthropic--claude-4.8-opus`, and
180
+ `gemini-3.5-flash`. Override them with `GPT_MODEL`, `BEDROCK_MODEL`, and
181
+ `VERTEX_MODEL`; set `SKIP_IMAGE=1` to skip vision tests.
182
+
163
183
  ## Models
164
184
 
165
185
  The model list is composed of three sources, merged at startup:
@@ -450,9 +470,10 @@ npmjs.com:
450
470
  │ └── publish.yml # tag-driven npm publish via OIDC trusted publishing
451
471
  ├── index.ts # ExtensionAPI factory + registerProvider calls (both providers)
452
472
  ├── scripts/
453
- │ ├── update-models.mjs # maintainer script: fetches models.dev, writes models-snapshot.json
454
- │ ├── list-sap-models.mjs # lists models your tenant actually deploys (diff vs snapshot)
455
- └── diagnose-streaming.mjs # probes orchestration streaming support per model
473
+ │ ├── update-models.mjs # maintainer script: fetches models.dev, writes models-snapshot.json
474
+ │ ├── list-sap-models.mjs # lists models your tenant actually deploys (diff vs snapshot)
475
+ ├── diagnose-streaming.mjs # probes orchestration streaming support per model
476
+ │ └── validate-foundation-executables.mjs # live text/tool/image smoke tests for direct foundation executables
456
477
  └── src/
457
478
  ├── auth.ts # service-key validation + pi oauth registration
458
479
  ├── model-catalog.ts # loads snapshot/cache/overlay and adapts models.dev metadata
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sap-aicore",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "SAP AI Core (orchestration + foundation) provider for the pi coding agent",
5
5
  "license": "MIT",
6
6
  "author": "Tim Pearson (https://github.com/ttiimmaahh)",
@@ -31,12 +31,14 @@
31
31
  },
32
32
  "scripts": {
33
33
  "update-models": "node scripts/update-models.mjs",
34
- "prepublishOnly": "tsc --noEmit"
34
+ "test": "node scripts/test-vertex-tool-schema.mjs",
35
+ "validate:foundation": "node scripts/validate-foundation-executables.mjs",
36
+ "prepublishOnly": "tsc --noEmit && npm test"
35
37
  },
36
38
  "dependencies": {
37
- "@sap-ai-sdk/ai-api": "^2.10.0",
38
- "@sap-ai-sdk/foundation-models": "^2.10.0",
39
- "@sap-ai-sdk/orchestration": "^2.10.0"
39
+ "@sap-ai-sdk/ai-api": "^2.12.0",
40
+ "@sap-ai-sdk/foundation-models": "^2.12.0",
41
+ "@sap-ai-sdk/orchestration": "^2.12.0"
40
42
  },
41
43
  "peerDependencies": {
42
44
  "@earendil-works/pi-ai": "*",
@@ -0,0 +1,59 @@
1
+ // Test-only pi extension: registers a `record_choice` tool whose parameter
2
+ // schema contains the exact JSON Schema constructs that Gemini's legacy
3
+ // OpenAPI `parameters` field rejects (`const`, boolean `enum`, nested anyOf).
4
+ //
5
+ // Loaded by scripts/validate-foundation-executables.mjs to exercise the real
6
+ // HTTP tool-schema path end-to-end. If an adapter serializes tool schemas
7
+ // incorrectly, the provider returns HTTP 400 and the live scenario fails.
8
+ //
9
+ // The tool simply writes its stringified args to OUTPUT_FILE so the validator
10
+ // can assert a real side effect (proving the model both received the schema
11
+ // and successfully issued a tool call against it).
12
+
13
+ import { writeFileSync } from "node:fs";
14
+
15
+ /** @param {import("@earendil-works/pi-coding-agent").ExtensionAPI} pi */
16
+ export default function (pi) {
17
+ pi.registerTool({
18
+ name: "record_choice",
19
+ label: "Record Choice",
20
+ description:
21
+ "Record a processing choice. Call this once with mode set to 'fast'.",
22
+ promptSnippet: "record_choice: record a processing choice",
23
+ // Raw JSON Schema (cast through unknown) with the constructs that broke
24
+ // Gemini's legacy `parameters` field. TypeBox typing is bypassed on
25
+ // purpose — we are validating wire serialization, not TS ergonomics.
26
+ parameters: /** @type {any} */ ({
27
+ type: "object",
28
+ properties: {
29
+ mode: {
30
+ anyOf: [{ const: "fast" }, { const: "slow" }],
31
+ description: "Processing mode",
32
+ },
33
+ verbose: {
34
+ anyOf: [{ type: "boolean" }, { enum: [false] }],
35
+ description: "Verbose flag",
36
+ },
37
+ steps: {
38
+ type: "array",
39
+ items: {
40
+ type: "object",
41
+ properties: {
42
+ kind: { anyOf: [{ const: "read" }, { const: "write" }] },
43
+ },
44
+ },
45
+ },
46
+ },
47
+ required: ["mode"],
48
+ }),
49
+ async execute(_toolCallId, params) {
50
+ const outputFile = process.env.COMPLEX_TOOL_OUTPUT_FILE;
51
+ if (outputFile) {
52
+ writeFileSync(outputFile, JSON.stringify(params ?? {}));
53
+ }
54
+ return {
55
+ content: [{ type: "text", text: "COMPLEX_TOOL_OK" }],
56
+ };
57
+ },
58
+ });
59
+ }
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ // Offline regression test for the Vertex/Gemini tool-schema translation.
3
+ //
4
+ // The live validator (validate-foundation-executables.mjs) only ever sent the
5
+ // trivial `bash` tool, whose schema is a flat object with one string property.
6
+ // That never exercised the JSON Schema constructs Gemini's legacy `parameters`
7
+ // (OpenAPI 3.0 subset) rejects — `const` and non-string `enum` values — so a
8
+ // real 400 in production slipped past a "green" validation run.
9
+ //
10
+ // This test asserts the translator emits `parametersJsonSchema` (full JSON
11
+ // Schema) rather than the legacy `parameters` field, and that the failing
12
+ // constructs survive the translation intact. It makes NO network calls.
13
+ //
14
+ // Usage from repo root:
15
+ // node scripts/test-vertex-tool-schema.mjs
16
+
17
+ import { pathToFileURL } from "node:url";
18
+ import { join } from "node:path";
19
+
20
+ const ROOT = new URL("..", import.meta.url).pathname;
21
+
22
+ const { piContextToVertexGenerateContent } = await import(
23
+ pathToFileURL(join(ROOT, "src/translate-foundation-vertexai.ts")).href
24
+ );
25
+
26
+ let failures = 0;
27
+ function check(condition, message) {
28
+ if (condition) {
29
+ console.log(` ✓ ${message}`);
30
+ return;
31
+ }
32
+ console.error(` ❌ ${message}`);
33
+ failures++;
34
+ }
35
+
36
+ // A schema that reproduces every class from the reported production 400:
37
+ // - `const` keyword → "Unknown name const"
38
+ // - boolean `enum` value (`false`) → "enum[0] (TYPE_STRING), false"
39
+ // - nested anyOf/items → deep property paths in the error
40
+ const context = {
41
+ systemPrompt: "system",
42
+ messages: [{ role: "user", content: "hi" }],
43
+ tools: [
44
+ {
45
+ name: "complex_tool",
46
+ description: "reproduces the reported Gemini 400 schema classes",
47
+ parameters: {
48
+ type: "object",
49
+ properties: {
50
+ mode: { anyOf: [{ const: "fast" }, { const: "slow" }] },
51
+ flag: { anyOf: [{ type: "boolean" }, { enum: [false] }] },
52
+ items: {
53
+ type: "array",
54
+ items: {
55
+ type: "object",
56
+ properties: {
57
+ kind: { anyOf: [{ const: "a" }, { const: "b" }] },
58
+ },
59
+ },
60
+ },
61
+ },
62
+ required: ["mode"],
63
+ },
64
+ },
65
+ ],
66
+ };
67
+
68
+ const out = piContextToVertexGenerateContent(context);
69
+ const fd = out.tools?.[0]?.functionDeclarations?.[0];
70
+
71
+ check(!!fd, "tool is translated to a functionDeclaration");
72
+ check(
73
+ fd && Object.hasOwn(fd, "parametersJsonSchema"),
74
+ "emits `parametersJsonSchema` (full JSON Schema field Gemini accepts)",
75
+ );
76
+ check(
77
+ fd && !Object.hasOwn(fd, "parameters"),
78
+ "does NOT emit legacy `parameters` (OpenAPI subset that 400s on const/enum)",
79
+ );
80
+
81
+ const serialized = JSON.stringify(fd?.parametersJsonSchema ?? {});
82
+ check(
83
+ serialized.includes('"const"'),
84
+ "`const` keyword survives translation (not stripped/rewritten)",
85
+ );
86
+ check(
87
+ serialized.includes("false"),
88
+ "boolean `enum` value survives translation",
89
+ );
90
+
91
+ if (failures > 0) {
92
+ console.error(`\n❌ vertex tool-schema test: ${failures} check(s) failed`);
93
+ process.exit(1);
94
+ }
95
+ console.log("\n✅ vertex tool-schema translation test passed");
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env node
2
+ // Live validation for SAP AI Core direct foundation executables.
3
+ //
4
+ // This script makes real, billed model calls through pi using the local extension.
5
+ // It validates the three high-value foundation executable families:
6
+ // - azure-openai → GPT/OpenAI models
7
+ // - aws-bedrock → Anthropic/Claude models
8
+ // - gcp-vertexai → Gemini models
9
+ //
10
+ // Each family now also runs a complex-tool-schema scenario (const/enum/anyOf)
11
+ // that reproduces the real HTTP tool path — the trivial bash tool alone never
12
+ // exercised the JSON Schema constructs that broke Gemini's `parameters` field.
13
+ //
14
+ // For a fast, credential-free regression check of the Vertex tool-schema
15
+ // serialization specifically, run:
16
+ // node scripts/test-vertex-tool-schema.mjs
17
+ //
18
+ // Usage from repo root:
19
+ // node scripts/validate-foundation-executables.mjs
20
+ //
21
+ // Optional model overrides:
22
+ // GPT_MODEL=gpt-5.5 \
23
+ // BEDROCK_MODEL=anthropic--claude-4.8-opus \
24
+ // VERTEX_MODEL=gemini-3.5-flash \
25
+ // node scripts/validate-foundation-executables.mjs
26
+ //
27
+ // Optional: SKIP_IMAGE=1 to skip vision tests.
28
+
29
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
30
+ import { tmpdir } from "node:os";
31
+ import { join } from "node:path";
32
+ import { spawnSync } from "node:child_process";
33
+
34
+ const ROOT = new URL("..", import.meta.url).pathname;
35
+ const PI = process.env.PI_BIN ?? "pi";
36
+ const SKIP_IMAGE = process.env.SKIP_IMAGE === "1";
37
+
38
+ const SCENARIOS = [
39
+ {
40
+ name: "azure-openai/gpt",
41
+ model: process.env.GPT_MODEL ?? "gpt-5.5",
42
+ marker: "GPT_TOOL_OK",
43
+ },
44
+ {
45
+ name: "aws-bedrock/anthropic",
46
+ model: process.env.BEDROCK_MODEL ?? "anthropic--claude-4.8-opus",
47
+ marker: "BEDROCK_TOOL_OK",
48
+ },
49
+ {
50
+ name: "gcp-vertexai/gemini",
51
+ model: process.env.VERTEX_MODEL ?? "gemini-3.5-flash",
52
+ marker: "VERTEX_TOOL_OK",
53
+ },
54
+ ];
55
+
56
+ const tinyPng = Buffer.from(
57
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=",
58
+ "base64",
59
+ );
60
+
61
+ function runPi(args, options = {}) {
62
+ const result = spawnSync(PI, args, {
63
+ cwd: ROOT,
64
+ encoding: "utf8",
65
+ maxBuffer: 10 * 1024 * 1024,
66
+ ...options,
67
+ });
68
+ return {
69
+ status: result.status ?? 1,
70
+ stdout: result.stdout ?? "",
71
+ stderr: result.stderr ?? "",
72
+ combined: `${result.stdout ?? ""}${result.stderr ?? ""}`,
73
+ };
74
+ }
75
+
76
+ function baseArgs(model, extraExtensions = []) {
77
+ const extensions = ["-e", "./index.ts"];
78
+ for (const ext of extraExtensions) extensions.push("-e", ext);
79
+ return [
80
+ "--no-extensions",
81
+ ...extensions,
82
+ "--model",
83
+ `sap-aicore-foundation/${model}`,
84
+ "--no-context-files",
85
+ "--no-skills",
86
+ "-p",
87
+ ];
88
+ }
89
+
90
+ function assert(condition, message, detail) {
91
+ if (condition) return;
92
+ console.error(`\n❌ ${message}`);
93
+ if (detail) console.error(detail);
94
+ process.exitCode = 1;
95
+ throw new Error(message);
96
+ }
97
+
98
+ function truncate(s, max = 2000) {
99
+ return s.length > max ? `${s.slice(0, max)}…[+${s.length - max} chars]` : s;
100
+ }
101
+
102
+ function validateText(scenario) {
103
+ const prompt = "Reply with exactly the single token: TEXT_OK";
104
+ const result = runPi([...baseArgs(scenario.model), prompt]);
105
+ assert(
106
+ result.status === 0,
107
+ `${scenario.name} text scenario exited ${result.status}`,
108
+ truncate(result.combined),
109
+ );
110
+ assert(
111
+ /TEXT_OK/.test(result.combined),
112
+ `${scenario.name} text scenario did not contain TEXT_OK`,
113
+ truncate(result.combined),
114
+ );
115
+ console.log(` ✓ text generation`);
116
+ }
117
+
118
+ function validateToolUse(scenario, workDir) {
119
+ const file = join(workDir, `${scenario.name.replaceAll("/", "-")}.txt`);
120
+ const prompt =
121
+ `Use the bash tool to run exactly: printf ${scenario.marker} > ${file}. ` +
122
+ "Then say done.";
123
+ const result = runPi([...baseArgs(scenario.model), prompt]);
124
+ assert(
125
+ result.status === 0,
126
+ `${scenario.name} tool scenario exited ${result.status}`,
127
+ truncate(result.combined),
128
+ );
129
+ let actual = "";
130
+ try {
131
+ actual = readFileSync(file, "utf8");
132
+ } catch {
133
+ // handled below
134
+ }
135
+ assert(
136
+ actual === scenario.marker,
137
+ `${scenario.name} tool scenario did not create expected side-effect`,
138
+ `expected file ${file} to contain ${scenario.marker}\nmodel output:\n${truncate(result.combined)}`,
139
+ );
140
+ console.log(` ✓ tool execution side effect`);
141
+ }
142
+
143
+ // Exercises the real HTTP tool-schema path with a schema containing the
144
+ // constructs that broke Gemini in production (`const`, boolean `enum`, nested
145
+ // anyOf). The trivial `bash` tool never covered these, which is why the
146
+ // production 400 slipped past an otherwise-green validation run.
147
+ function validateComplexToolSchema(scenario, workDir) {
148
+ const extension = join(ROOT, "scripts/fixtures/complex-tool-extension.mjs");
149
+ const outputFile = join(
150
+ workDir,
151
+ `${scenario.name.replaceAll("/", "-")}-complex-tool.json`,
152
+ );
153
+ const prompt =
154
+ "Call the record_choice tool exactly once with mode set to 'fast'. " +
155
+ "Then say done.";
156
+ const result = runPi([...baseArgs(scenario.model, [extension]), prompt], {
157
+ env: { ...process.env, COMPLEX_TOOL_OUTPUT_FILE: outputFile },
158
+ });
159
+ assert(
160
+ !/status code 400|Invalid (JSON payload|value at)|Unknown name/.test(
161
+ result.combined,
162
+ ),
163
+ `${scenario.name} complex-tool scenario hit a schema rejection (HTTP 400)`,
164
+ truncate(result.combined),
165
+ );
166
+ assert(
167
+ result.status === 0,
168
+ `${scenario.name} complex-tool scenario exited ${result.status}`,
169
+ truncate(result.combined),
170
+ );
171
+ let actual = "";
172
+ try {
173
+ actual = readFileSync(outputFile, "utf8");
174
+ } catch {
175
+ // handled below
176
+ }
177
+ assert(
178
+ /"mode"\s*:\s*"fast"/.test(actual),
179
+ `${scenario.name} complex-tool scenario did not produce a valid tool call`,
180
+ `expected ${outputFile} to contain mode=fast\nmodel output:\n${truncate(result.combined)}`,
181
+ );
182
+ console.log(` \u2713 complex tool schema (const/enum/anyOf)`);
183
+ }
184
+
185
+ function validateImage(scenario, workDir) {
186
+ const imagePath = join(workDir, "tiny.png");
187
+ writeFileSync(imagePath, tinyPng);
188
+ const prompt =
189
+ "Look at @" +
190
+ imagePath +
191
+ " and reply with exactly IMAGE_OK if you can inspect the attached image.";
192
+ const result = runPi([...baseArgs(scenario.model), prompt]);
193
+ assert(
194
+ result.status === 0,
195
+ `${scenario.name} image scenario exited ${result.status}`,
196
+ truncate(result.combined),
197
+ );
198
+ assert(
199
+ /IMAGE_OK/.test(result.combined),
200
+ `${scenario.name} image scenario did not contain IMAGE_OK`,
201
+ truncate(result.combined),
202
+ );
203
+ console.log(` ✓ image input smoke test`);
204
+ }
205
+
206
+ const workDir = mkdtempSync(join(tmpdir(), "pi-sap-aicore-foundation-"));
207
+ console.log(`Live SAP AI Core foundation validation`);
208
+ console.log(`repo: ${ROOT}`);
209
+ console.log(`workDir: ${workDir}`);
210
+ console.log(`skip image: ${SKIP_IMAGE ? "yes" : "no"}\n`);
211
+
212
+ try {
213
+ for (const scenario of SCENARIOS) {
214
+ console.log(`${scenario.name} model=${scenario.model}`);
215
+ validateText(scenario);
216
+ validateToolUse(scenario, workDir);
217
+ validateComplexToolSchema(scenario, workDir);
218
+ if (!SKIP_IMAGE) validateImage(scenario, workDir);
219
+ console.log("");
220
+ }
221
+ console.log("✅ all foundation executable scenarios passed");
222
+ } catch {
223
+ // assert() already printed details and set exitCode.
224
+ } finally {
225
+ if (process.env.KEEP_VALIDATION_ARTIFACTS !== "1") {
226
+ rmSync(workDir, { recursive: true, force: true });
227
+ } else {
228
+ console.log(`kept artifacts: ${workDir}`);
229
+ }
230
+ }
package/src/stream.ts CHANGED
@@ -363,6 +363,7 @@ function reasoningParams(
363
363
  model: Model<Api>,
364
364
  reasoning: string | undefined,
365
365
  effectiveMaxTokens: number,
366
+ hasTools: boolean,
366
367
  ): Partial<LlmModelParams> {
367
368
  if (!reasoning || reasoning === "off") return {};
368
369
 
@@ -402,6 +403,21 @@ function reasoningParams(
402
403
  };
403
404
  }
404
405
  if (model.id.startsWith("gpt-")) {
406
+ // SAP orchestration's chat/completions endpoint does NOT accept
407
+ // `reasoning_effort` alongside function tools for gpt-5.x. Confirmed
408
+ // against SAP's Supported Parameters list (which omits reasoning_effort
409
+ // entirely) and the runtime 400 from gpt-5.5:
410
+ // "Function tools with reasoning_effort are not supported for
411
+ // gpt-5.5-...-dz-std in /v1/chat/completions. Please use /v1/responses
412
+ // instead."
413
+ // The Responses API (/v1/responses) is a foundation-models deployment
414
+ // endpoint, unreachable through the orchestration SDK, so we can't switch
415
+ // endpoints here. Drop reasoning_effort on tool turns so the request
416
+ // succeeds; SAP still spends reasoning tokens internally (governed by
417
+ // max_completion_tokens / max_tokens). Text-only turns keep explicit
418
+ // reasoning depth. Applies to all gpt-* since SAP may tighten the older
419
+ // gpt-5/5.4 ids the same way.
420
+ if (hasTools) return {};
405
421
  const effort =
406
422
  model.thinkingLevelMap?.[
407
423
  reasoning as keyof NonNullable<typeof model.thinkingLevelMap>
@@ -422,6 +438,7 @@ function modelSupportsTemperature(modelId: string): boolean {
422
438
  function buildLlmParams(
423
439
  model: Model<Api>,
424
440
  options: SimpleStreamOptions | undefined,
441
+ hasTools: boolean,
425
442
  ): LlmModelParams {
426
443
  // Pi may pass a maxTokens budget smaller than the model's hard cap (e.g.
427
444
  // to reserve room for thinking). Respect it; otherwise fall back to the
@@ -431,6 +448,7 @@ function buildLlmParams(
431
448
  model,
432
449
  options?.reasoning,
433
450
  effectiveMaxTokens,
451
+ hasTools,
434
452
  );
435
453
  const params: LlmModelParams = {
436
454
  max_tokens: effectiveMaxTokens,
@@ -655,7 +673,7 @@ export function streamSapAiCore(
655
673
  const { messages, tools } = piContextToOrchestration(context);
656
674
 
657
675
  const { OrchestrationClient } = await importOrchestration();
658
- const llmParams = buildLlmParams(model, options);
676
+ const llmParams = buildLlmParams(model, options, tools.length > 0);
659
677
 
660
678
  debugLog({
661
679
  requestId,
@@ -23,7 +23,11 @@ export type VertexTool = {
23
23
  functionDeclarations: Array<{
24
24
  name: string;
25
25
  description: string;
26
- parameters: Record<string, unknown>;
26
+ // Gemini's `parameters` field is a restricted OpenAPI 3.0 Schema subset
27
+ // that rejects `const` and non-string `enum` values. `parametersJsonSchema`
28
+ // accepts full JSON Schema (anyOf/oneOf/const/boolean enums), which is what
29
+ // pi tool definitions actually use. See pi-ai google-shared convertTools.
30
+ parametersJsonSchema: Record<string, unknown>;
27
31
  }>;
28
32
  };
29
33
 
@@ -139,7 +143,11 @@ function piToolToVertexFunctionDeclaration(
139
143
  return {
140
144
  name: tool.name,
141
145
  description: tool.description,
142
- parameters: tool.parameters as unknown as Record<string, unknown>,
146
+ // Use `parametersJsonSchema` (full JSON Schema) instead of the legacy
147
+ // `parameters` OpenAPI subset. The latter rejects `const` and non-string
148
+ // `enum` values that pi tool schemas commonly contain, causing HTTP 400
149
+ // "Unknown name const" / "enum[0] (TYPE_STRING)" errors from Gemini.
150
+ parametersJsonSchema: tool.parameters as unknown as Record<string, unknown>,
143
151
  };
144
152
  }
145
153