gsd-pi 2.68.1-dev.362687a → 2.68.1-dev.58193fa

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 (143) hide show
  1. package/dist/resources/extensions/gsd/auto-model-selection.js +27 -1
  2. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +7 -0
  3. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +1 -5
  4. package/dist/resources/extensions/gsd/codebase-generator.js +12 -0
  5. package/dist/resources/extensions/gsd/guided-flow.js +25 -70
  6. package/dist/resources/extensions/gsd/model-router.js +85 -2
  7. package/dist/resources/extensions/gsd/prompts/discuss.md +2 -0
  8. package/dist/resources/extensions/gsd/templates/context.md +34 -2
  9. package/dist/web/standalone/.next/BUILD_ID +1 -1
  10. package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
  11. package/dist/web/standalone/.next/build-manifest.json +2 -2
  12. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  13. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/index.html +1 -1
  30. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
  37. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  38. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  39. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  40. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  41. package/package.json +1 -1
  42. package/packages/pi-ai/dist/index.d.ts +3 -0
  43. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  44. package/packages/pi-ai/dist/index.js +2 -0
  45. package/packages/pi-ai/dist/index.js.map +1 -1
  46. package/packages/pi-ai/dist/providers/amazon-bedrock.js +2 -2
  47. package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
  48. package/packages/pi-ai/dist/providers/anthropic-shared.js +2 -2
  49. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  50. package/packages/pi-ai/dist/providers/google-shared.js +2 -2
  51. package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
  52. package/packages/pi-ai/dist/providers/mistral.js +2 -2
  53. package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
  54. package/packages/pi-ai/dist/providers/openai-completions.js +2 -2
  55. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  56. package/packages/pi-ai/dist/providers/openai-responses-shared.js +2 -2
  57. package/packages/pi-ai/dist/providers/openai-responses-shared.js.map +1 -1
  58. package/packages/pi-ai/dist/providers/provider-capabilities.d.ts +59 -0
  59. package/packages/pi-ai/dist/providers/provider-capabilities.d.ts.map +1 -0
  60. package/packages/pi-ai/dist/providers/provider-capabilities.js +173 -0
  61. package/packages/pi-ai/dist/providers/provider-capabilities.js.map +1 -0
  62. package/packages/pi-ai/dist/providers/provider-capabilities.test.d.ts +2 -0
  63. package/packages/pi-ai/dist/providers/provider-capabilities.test.d.ts.map +1 -0
  64. package/packages/pi-ai/dist/providers/provider-capabilities.test.js +132 -0
  65. package/packages/pi-ai/dist/providers/provider-capabilities.test.js.map +1 -0
  66. package/packages/pi-ai/dist/providers/transform-messages-report.test.d.ts +2 -0
  67. package/packages/pi-ai/dist/providers/transform-messages-report.test.d.ts.map +1 -0
  68. package/packages/pi-ai/dist/providers/transform-messages-report.test.js +172 -0
  69. package/packages/pi-ai/dist/providers/transform-messages-report.test.js.map +1 -0
  70. package/packages/pi-ai/dist/providers/transform-messages.d.ts +34 -1
  71. package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
  72. package/packages/pi-ai/dist/providers/transform-messages.js +73 -2
  73. package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
  74. package/packages/pi-ai/src/index.ts +3 -0
  75. package/packages/pi-ai/src/providers/amazon-bedrock.ts +2 -2
  76. package/packages/pi-ai/src/providers/anthropic-shared.ts +2 -2
  77. package/packages/pi-ai/src/providers/google-shared.ts +2 -2
  78. package/packages/pi-ai/src/providers/mistral.ts +2 -2
  79. package/packages/pi-ai/src/providers/openai-completions.ts +2 -2
  80. package/packages/pi-ai/src/providers/openai-responses-shared.ts +2 -2
  81. package/packages/pi-ai/src/providers/provider-capabilities.test.ts +174 -0
  82. package/packages/pi-ai/src/providers/provider-capabilities.ts +215 -0
  83. package/packages/pi-ai/src/providers/transform-messages-report.test.ts +189 -0
  84. package/packages/pi-ai/src/providers/transform-messages.ts +94 -1
  85. package/packages/pi-coding-agent/dist/core/extensions/index.d.ts +1 -1
  86. package/packages/pi-coding-agent/dist/core/extensions/index.d.ts.map +1 -1
  87. package/packages/pi-coding-agent/dist/core/extensions/index.js.map +1 -1
  88. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/extensions/loader.js +10 -1
  90. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -1
  92. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/extensions/runner.js +15 -0
  94. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +41 -0
  96. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -0
  99. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  100. package/packages/pi-coding-agent/dist/core/tools/index.js +1 -0
  101. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/tools/tool-compatibility-registry.d.ts +27 -0
  103. package/packages/pi-coding-agent/dist/core/tools/tool-compatibility-registry.d.ts.map +1 -0
  104. package/packages/pi-coding-agent/dist/core/tools/tool-compatibility-registry.js +69 -0
  105. package/packages/pi-coding-agent/dist/core/tools/tool-compatibility-registry.js.map +1 -0
  106. package/packages/pi-coding-agent/dist/index.d.ts +2 -2
  107. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/index.js +3 -1
  109. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  110. package/packages/pi-coding-agent/src/core/extensions/index.ts +4 -0
  111. package/packages/pi-coding-agent/src/core/extensions/loader.ts +11 -1
  112. package/packages/pi-coding-agent/src/core/extensions/runner.ts +18 -0
  113. package/packages/pi-coding-agent/src/core/extensions/types.ts +45 -0
  114. package/packages/pi-coding-agent/src/core/tools/index.ts +7 -0
  115. package/packages/pi-coding-agent/src/core/tools/tool-compatibility-registry.ts +83 -0
  116. package/packages/pi-coding-agent/src/index.ts +9 -0
  117. package/src/resources/extensions/gsd/auto-model-selection.ts +36 -4
  118. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +8 -0
  119. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +1 -5
  120. package/src/resources/extensions/gsd/codebase-generator.ts +16 -0
  121. package/src/resources/extensions/gsd/guided-flow.ts +22 -84
  122. package/src/resources/extensions/gsd/model-router.ts +117 -10
  123. package/src/resources/extensions/gsd/preferences-types.ts +3 -1
  124. package/src/resources/extensions/gsd/prompts/discuss.md +2 -0
  125. package/src/resources/extensions/gsd/templates/context.md +34 -2
  126. package/src/resources/extensions/gsd/tests/capability-router.test.ts +31 -7
  127. package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +28 -0
  128. package/src/resources/extensions/gsd/tests/model-router.test.ts +2 -2
  129. package/src/resources/extensions/gsd/tests/tool-compatibility.test.ts +199 -0
  130. package/src/resources/extensions/gsd/tests/write-gate.test.ts +13 -16
  131. package/dist/resources/extensions/gsd/prompt-validation.js +0 -67
  132. package/dist/resources/extensions/gsd/prompts/discuss-prepared.md +0 -424
  133. package/dist/resources/extensions/gsd/templates/context-enhanced.md +0 -138
  134. package/src/resources/extensions/gsd/prompt-validation.ts +0 -88
  135. package/src/resources/extensions/gsd/prompts/discuss-prepared.md +0 -424
  136. package/src/resources/extensions/gsd/templates/context-enhanced.md +0 -138
  137. package/src/resources/extensions/gsd/tests/adversarial-review-fixes.test.ts +0 -223
  138. package/src/resources/extensions/gsd/tests/integration/test-isolation.ts +0 -53
  139. package/src/resources/extensions/gsd/tests/integration-prepared-discussion.test.ts +0 -525
  140. package/src/resources/extensions/gsd/tests/preparation.test.ts +0 -1211
  141. package/src/resources/extensions/gsd/tests/prompt-builder.test.ts +0 -669
  142. /package/dist/web/standalone/.next/static/{VkiZZ5UjK7EfSjrWWd5RC → YFZaRxYFkrifCiWU3AcrJ}/_buildManifest.js +0 -0
  143. /package/dist/web/standalone/.next/static/{VkiZZ5UjK7EfSjrWWd5RC → YFZaRxYFkrifCiWU3AcrJ}/_ssgManifest.js +0 -0
@@ -0,0 +1,174 @@
1
+ // GSD-2 — Provider Capabilities Registry Tests (ADR-005 Phase 1)
2
+ import { describe, test } from "node:test";
3
+ import assert from "node:assert/strict";
4
+
5
+ import {
6
+ PROVIDER_CAPABILITIES,
7
+ getProviderCapabilities,
8
+ getUnsupportedFeatures,
9
+ mergeCapabilityOverrides,
10
+ getRegisteredApis,
11
+ } from "./provider-capabilities.js";
12
+
13
+ // ─── Registry Completeness ──────────────────────────────────────────────────
14
+
15
+ describe("PROVIDER_CAPABILITIES registry", () => {
16
+ const EXPECTED_APIS = [
17
+ "anthropic-messages",
18
+ "anthropic-vertex",
19
+ "openai-responses",
20
+ "azure-openai-responses",
21
+ "openai-codex-responses",
22
+ "openai-completions",
23
+ "google-generative-ai",
24
+ "google-gemini-cli",
25
+ "google-vertex",
26
+ "mistral-conversations",
27
+ "bedrock-converse-stream",
28
+ "ollama-chat",
29
+ ];
30
+
31
+ test("covers all expected API providers", () => {
32
+ for (const api of EXPECTED_APIS) {
33
+ assert.ok(
34
+ PROVIDER_CAPABILITIES[api],
35
+ `Missing capability entry for API: ${api}`,
36
+ );
37
+ }
38
+ });
39
+
40
+ test("getRegisteredApis returns all entries", () => {
41
+ const registered = getRegisteredApis();
42
+ for (const api of EXPECTED_APIS) {
43
+ assert.ok(registered.includes(api), `getRegisteredApis missing: ${api}`);
44
+ }
45
+ });
46
+
47
+ test("all entries have required fields", () => {
48
+ for (const [api, caps] of Object.entries(PROVIDER_CAPABILITIES)) {
49
+ assert.equal(typeof caps.toolCalling, "boolean", `${api}.toolCalling`);
50
+ assert.equal(typeof caps.maxTools, "number", `${api}.maxTools`);
51
+ assert.equal(typeof caps.imageToolResults, "boolean", `${api}.imageToolResults`);
52
+ assert.equal(typeof caps.structuredOutput, "boolean", `${api}.structuredOutput`);
53
+ assert.ok(caps.toolCallIdFormat, `${api}.toolCallIdFormat`);
54
+ assert.equal(typeof caps.toolCallIdFormat.maxLength, "number", `${api}.toolCallIdFormat.maxLength`);
55
+ assert.ok(caps.toolCallIdFormat.allowedChars instanceof RegExp, `${api}.toolCallIdFormat.allowedChars`);
56
+ assert.ok(
57
+ ["full", "text-only", "none"].includes(caps.thinkingPersistence),
58
+ `${api}.thinkingPersistence is "${caps.thinkingPersistence}"`,
59
+ );
60
+ assert.ok(Array.isArray(caps.unsupportedSchemaFeatures), `${api}.unsupportedSchemaFeatures`);
61
+ }
62
+ });
63
+ });
64
+
65
+ // ─── Provider-specific Values ───────────────────────────────────────────────
66
+
67
+ describe("provider-specific capabilities", () => {
68
+ test("Anthropic supports full thinking persistence", () => {
69
+ assert.equal(PROVIDER_CAPABILITIES["anthropic-messages"].thinkingPersistence, "full");
70
+ });
71
+
72
+ test("Anthropic supports image tool results", () => {
73
+ assert.equal(PROVIDER_CAPABILITIES["anthropic-messages"].imageToolResults, true);
74
+ });
75
+
76
+ test("Anthropic tool call ID is 64 chars max", () => {
77
+ assert.equal(PROVIDER_CAPABILITIES["anthropic-messages"].toolCallIdFormat.maxLength, 64);
78
+ });
79
+
80
+ test("Mistral tool call ID is 9 chars max", () => {
81
+ assert.equal(PROVIDER_CAPABILITIES["mistral-conversations"].toolCallIdFormat.maxLength, 9);
82
+ });
83
+
84
+ test("Mistral has no thinking persistence", () => {
85
+ assert.equal(PROVIDER_CAPABILITIES["mistral-conversations"].thinkingPersistence, "none");
86
+ });
87
+
88
+ test("Google does not support patternProperties", () => {
89
+ assert.ok(
90
+ PROVIDER_CAPABILITIES["google-generative-ai"].unsupportedSchemaFeatures.includes("patternProperties"),
91
+ );
92
+ });
93
+
94
+ test("Google does not support const", () => {
95
+ assert.ok(
96
+ PROVIDER_CAPABILITIES["google-generative-ai"].unsupportedSchemaFeatures.includes("const"),
97
+ );
98
+ });
99
+
100
+ test("OpenAI Responses does not support image tool results", () => {
101
+ assert.equal(PROVIDER_CAPABILITIES["openai-responses"].imageToolResults, false);
102
+ });
103
+
104
+ test("OpenAI Responses has text-only thinking persistence", () => {
105
+ assert.equal(PROVIDER_CAPABILITIES["openai-responses"].thinkingPersistence, "text-only");
106
+ });
107
+ });
108
+
109
+ // ─── getProviderCapabilities ────────────────────────────────────────────────
110
+
111
+ describe("getProviderCapabilities", () => {
112
+ test("returns known provider capabilities", () => {
113
+ const caps = getProviderCapabilities("anthropic-messages");
114
+ assert.equal(caps.toolCalling, true);
115
+ assert.equal(caps.thinkingPersistence, "full");
116
+ });
117
+
118
+ test("returns permissive defaults for unknown providers", () => {
119
+ const caps = getProviderCapabilities("unknown-provider-xyz");
120
+ assert.equal(caps.toolCalling, true);
121
+ assert.equal(caps.imageToolResults, true);
122
+ assert.deepEqual(caps.unsupportedSchemaFeatures, []);
123
+ });
124
+ });
125
+
126
+ // ─── getUnsupportedFeatures ─────────────────────────────────────────────────
127
+
128
+ describe("getUnsupportedFeatures", () => {
129
+ test("returns unsupported features for Google", () => {
130
+ const unsupported = getUnsupportedFeatures("google-generative-ai", ["patternProperties", "const"]);
131
+ assert.deepEqual(unsupported, ["patternProperties", "const"]);
132
+ });
133
+
134
+ test("returns empty for Anthropic with any features", () => {
135
+ const unsupported = getUnsupportedFeatures("anthropic-messages", ["patternProperties", "const"]);
136
+ assert.deepEqual(unsupported, []);
137
+ });
138
+
139
+ test("returns empty for unknown provider", () => {
140
+ const unsupported = getUnsupportedFeatures("unknown-xyz", ["patternProperties"]);
141
+ assert.deepEqual(unsupported, []);
142
+ });
143
+ });
144
+
145
+ // ─── mergeCapabilityOverrides ───────────────────────────────────────────────
146
+
147
+ describe("mergeCapabilityOverrides", () => {
148
+ test("overrides individual fields", () => {
149
+ const merged = mergeCapabilityOverrides("openai-responses", {
150
+ imageToolResults: true,
151
+ });
152
+ assert.equal(merged.imageToolResults, true);
153
+ // Non-overridden fields preserved
154
+ assert.equal(merged.toolCalling, true);
155
+ assert.equal(merged.thinkingPersistence, "text-only");
156
+ });
157
+
158
+ test("deep-merges toolCallIdFormat", () => {
159
+ const merged = mergeCapabilityOverrides("anthropic-messages", {
160
+ toolCallIdFormat: { maxLength: 128 },
161
+ });
162
+ assert.equal(merged.toolCallIdFormat.maxLength, 128);
163
+ // allowedChars preserved from base
164
+ assert.ok(merged.toolCallIdFormat.allowedChars instanceof RegExp);
165
+ });
166
+
167
+ test("uses permissive defaults for unknown provider", () => {
168
+ const merged = mergeCapabilityOverrides("unknown-xyz", {
169
+ imageToolResults: false,
170
+ });
171
+ assert.equal(merged.imageToolResults, false);
172
+ assert.equal(merged.toolCalling, true); // from default
173
+ });
174
+ });
@@ -0,0 +1,215 @@
1
+ // GSD-2 — Provider Capabilities Registry (ADR-005 Phase 1)
2
+ // Declarative registry of what each API provider supports, consolidating
3
+ // scattered knowledge from *-shared.ts files into a queryable data structure.
4
+
5
+ import type { Api } from "../types.js";
6
+
7
+ // ─── Types ──────────────────────────────────────────────────────────────────
8
+
9
+ /**
10
+ * Declarative capability profile for an API provider.
11
+ * Used by the model router to filter incompatible models and by the tool
12
+ * system to adjust tool sets per provider.
13
+ */
14
+ export interface ProviderCapabilities {
15
+ /** Whether models from this provider support tool/function calling */
16
+ toolCalling: boolean;
17
+ /** Maximum number of tools the provider handles well (0 = unlimited) */
18
+ maxTools: number;
19
+ /** Whether tool results can contain images */
20
+ imageToolResults: boolean;
21
+ /** Whether the provider supports structured JSON output */
22
+ structuredOutput: boolean;
23
+ /** Tool call ID format constraints */
24
+ toolCallIdFormat: {
25
+ maxLength: number;
26
+ allowedChars: RegExp;
27
+ };
28
+ /** Whether thinking/reasoning blocks are preserved cross-turn */
29
+ thinkingPersistence: "full" | "text-only" | "none";
30
+ /** Schema features NOT supported (tools using these get filtered) */
31
+ unsupportedSchemaFeatures: string[];
32
+ }
33
+
34
+ // ─── Registry ───────────────────────────────────────────────────────────────
35
+
36
+ /**
37
+ * Built-in provider capability profiles.
38
+ *
39
+ * Sources (consolidated from scattered *-shared.ts files):
40
+ * - anthropic-shared.ts: normalizeToolCallId (64-char, [a-zA-Z0-9_-])
41
+ * - openai-responses-shared.ts: ID normalization (64-char, fc_ prefix), image-in-tool-result workaround
42
+ * - google-shared.ts: sanitizeSchemaForGoogle (patternProperties, const), requiresToolCallId
43
+ * - mistral.ts: MISTRAL_TOOL_CALL_ID_LENGTH = 9
44
+ * - amazon-bedrock.ts: normalizeToolCallId (64-char, [a-zA-Z0-9_-])
45
+ */
46
+ export const PROVIDER_CAPABILITIES: Record<string, ProviderCapabilities> = {
47
+ "anthropic-messages": {
48
+ toolCalling: true,
49
+ maxTools: 0,
50
+ imageToolResults: true,
51
+ structuredOutput: true,
52
+ toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ },
53
+ thinkingPersistence: "full",
54
+ unsupportedSchemaFeatures: [],
55
+ },
56
+ "anthropic-vertex": {
57
+ toolCalling: true,
58
+ maxTools: 0,
59
+ imageToolResults: true,
60
+ structuredOutput: true,
61
+ toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ },
62
+ thinkingPersistence: "full",
63
+ unsupportedSchemaFeatures: [],
64
+ },
65
+ "openai-responses": {
66
+ toolCalling: true,
67
+ maxTools: 0,
68
+ imageToolResults: false, // images sent as separate user message, not in tool result
69
+ structuredOutput: true,
70
+ toolCallIdFormat: { maxLength: 512, allowedChars: /^.+$/ },
71
+ thinkingPersistence: "text-only",
72
+ unsupportedSchemaFeatures: [],
73
+ },
74
+ "azure-openai-responses": {
75
+ toolCalling: true,
76
+ maxTools: 0,
77
+ imageToolResults: false,
78
+ structuredOutput: true,
79
+ toolCallIdFormat: { maxLength: 512, allowedChars: /^.+$/ },
80
+ thinkingPersistence: "text-only",
81
+ unsupportedSchemaFeatures: [],
82
+ },
83
+ "openai-codex-responses": {
84
+ toolCalling: true,
85
+ maxTools: 0,
86
+ imageToolResults: false,
87
+ structuredOutput: true,
88
+ toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ },
89
+ thinkingPersistence: "text-only",
90
+ unsupportedSchemaFeatures: [],
91
+ },
92
+ "openai-completions": {
93
+ toolCalling: true,
94
+ maxTools: 0,
95
+ imageToolResults: false,
96
+ structuredOutput: true,
97
+ toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ },
98
+ thinkingPersistence: "text-only",
99
+ unsupportedSchemaFeatures: [],
100
+ },
101
+ "google-generative-ai": {
102
+ toolCalling: true,
103
+ maxTools: 0,
104
+ imageToolResults: true,
105
+ structuredOutput: true,
106
+ toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ },
107
+ thinkingPersistence: "text-only",
108
+ unsupportedSchemaFeatures: ["patternProperties", "const"],
109
+ },
110
+ "google-gemini-cli": {
111
+ toolCalling: true,
112
+ maxTools: 0,
113
+ imageToolResults: true,
114
+ structuredOutput: true,
115
+ toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ },
116
+ thinkingPersistence: "text-only",
117
+ unsupportedSchemaFeatures: ["patternProperties", "const"],
118
+ },
119
+ "google-vertex": {
120
+ toolCalling: true,
121
+ maxTools: 0,
122
+ imageToolResults: true,
123
+ structuredOutput: true,
124
+ toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ },
125
+ thinkingPersistence: "text-only",
126
+ unsupportedSchemaFeatures: ["patternProperties", "const"],
127
+ },
128
+ "mistral-conversations": {
129
+ toolCalling: true,
130
+ maxTools: 0,
131
+ imageToolResults: false,
132
+ structuredOutput: true,
133
+ toolCallIdFormat: { maxLength: 9, allowedChars: /^[a-zA-Z0-9]+$/ },
134
+ thinkingPersistence: "none",
135
+ unsupportedSchemaFeatures: [],
136
+ },
137
+ "bedrock-converse-stream": {
138
+ toolCalling: true,
139
+ maxTools: 0,
140
+ imageToolResults: true, // Bedrock supports image content blocks in tool results
141
+ structuredOutput: true,
142
+ toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ },
143
+ thinkingPersistence: "text-only",
144
+ unsupportedSchemaFeatures: [],
145
+ },
146
+ "ollama-chat": {
147
+ toolCalling: true,
148
+ maxTools: 0,
149
+ imageToolResults: false,
150
+ structuredOutput: false,
151
+ toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ },
152
+ thinkingPersistence: "none",
153
+ unsupportedSchemaFeatures: [],
154
+ },
155
+ };
156
+
157
+ // ─── Default (permissive) profile for unknown providers ─────────────────────
158
+
159
+ const DEFAULT_CAPABILITIES: ProviderCapabilities = {
160
+ toolCalling: true,
161
+ maxTools: 0,
162
+ imageToolResults: true,
163
+ structuredOutput: true,
164
+ toolCallIdFormat: { maxLength: 512, allowedChars: /^.+$/ },
165
+ thinkingPersistence: "text-only",
166
+ unsupportedSchemaFeatures: [],
167
+ };
168
+
169
+ // ─── Public API ─────────────────────────────────────────────────────────────
170
+
171
+ /**
172
+ * Get capabilities for a provider API. Returns a permissive default for
173
+ * unknown providers (preserving existing behavior per ADR-005 principle 5).
174
+ */
175
+ export function getProviderCapabilities(api: string): ProviderCapabilities {
176
+ return PROVIDER_CAPABILITIES[api] ?? DEFAULT_CAPABILITIES;
177
+ }
178
+
179
+ /**
180
+ * Check if a provider supports all required schema features.
181
+ * Returns the list of unsupported features (empty if all supported).
182
+ */
183
+ export function getUnsupportedFeatures(api: string, requiredFeatures: string[]): string[] {
184
+ const caps = getProviderCapabilities(api);
185
+ return requiredFeatures.filter(f => caps.unsupportedSchemaFeatures.includes(f));
186
+ }
187
+
188
+ /**
189
+ * Deep-merge user-provided capability overrides with built-in defaults.
190
+ * Partial overrides merge with the built-in profile for the given API.
191
+ */
192
+ export function mergeCapabilityOverrides(
193
+ api: string,
194
+ overrides: Partial<Omit<ProviderCapabilities, "toolCallIdFormat">> & {
195
+ toolCallIdFormat?: Partial<ProviderCapabilities["toolCallIdFormat"]>;
196
+ },
197
+ ): ProviderCapabilities {
198
+ const base = getProviderCapabilities(api);
199
+ return {
200
+ ...base,
201
+ ...overrides,
202
+ toolCallIdFormat: overrides.toolCallIdFormat
203
+ ? { ...base.toolCallIdFormat, ...overrides.toolCallIdFormat }
204
+ : base.toolCallIdFormat,
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Get all registered API names in the capability registry.
210
+ * Used by lint rules to verify all providers in register-builtins.ts
211
+ * have corresponding capability entries.
212
+ */
213
+ export function getRegisteredApis(): string[] {
214
+ return Object.keys(PROVIDER_CAPABILITIES);
215
+ }
@@ -0,0 +1,189 @@
1
+ // GSD-2 — ProviderSwitchReport Tests (ADR-005 Phase 3)
2
+ import { describe, test } from "node:test";
3
+ import assert from "node:assert/strict";
4
+
5
+ import { transformMessages, createEmptyReport, hasTransformations } from "./transform-messages.js";
6
+ import type { ProviderSwitchReport } from "./transform-messages.js";
7
+ import type { Message, Model, AssistantMessage, ToolCall } from "../types.js";
8
+
9
+ // ─── Helpers ────────────────────────────────────────────────────────────────
10
+
11
+ function makeModel(overrides: Partial<Model<any>> = {}): Model<any> {
12
+ return {
13
+ id: "claude-sonnet-4-6",
14
+ name: "Claude Sonnet 4.6",
15
+ api: "anthropic-messages",
16
+ provider: "anthropic",
17
+ baseUrl: "",
18
+ reasoning: false,
19
+ input: ["text", "image"],
20
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
21
+ contextWindow: 200000,
22
+ maxTokens: 8192,
23
+ ...overrides,
24
+ } as Model<any>;
25
+ }
26
+
27
+ function makeAssistantMsg(overrides: Partial<AssistantMessage> = {}): AssistantMessage {
28
+ return {
29
+ role: "assistant",
30
+ content: [],
31
+ api: "anthropic-messages",
32
+ provider: "anthropic",
33
+ model: "claude-sonnet-4-6",
34
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
35
+ stopReason: "stop",
36
+ timestamp: Date.now(),
37
+ ...overrides,
38
+ };
39
+ }
40
+
41
+ // ─── createEmptyReport / hasTransformations ─────────────────────────────────
42
+
43
+ describe("createEmptyReport", () => {
44
+ test("creates report with zero counters", () => {
45
+ const report = createEmptyReport("anthropic-messages", "openai-responses");
46
+ assert.equal(report.fromApi, "anthropic-messages");
47
+ assert.equal(report.toApi, "openai-responses");
48
+ assert.equal(report.thinkingBlocksDropped, 0);
49
+ assert.equal(report.thinkingBlocksDowngraded, 0);
50
+ assert.equal(report.toolCallIdsRemapped, 0);
51
+ assert.equal(report.syntheticToolResultsInserted, 0);
52
+ assert.equal(report.thoughtSignaturesDropped, 0);
53
+ });
54
+ });
55
+
56
+ describe("hasTransformations", () => {
57
+ test("returns false for empty report", () => {
58
+ const report = createEmptyReport("a", "b");
59
+ assert.equal(hasTransformations(report), false);
60
+ });
61
+
62
+ test("returns true when any counter is non-zero", () => {
63
+ const report = createEmptyReport("a", "b");
64
+ report.thinkingBlocksDropped = 1;
65
+ assert.equal(hasTransformations(report), true);
66
+ });
67
+ });
68
+
69
+ // ─── Report Tracking in transformMessages ───────────────────────────────────
70
+
71
+ describe("transformMessages with report tracking", () => {
72
+ test("tracks thinking blocks dropped for redacted cross-model", () => {
73
+ const model = makeModel({ id: "gpt-5", api: "openai-responses", provider: "openai" });
74
+ const messages: Message[] = [
75
+ makeAssistantMsg({
76
+ content: [
77
+ { type: "thinking", thinking: "", redacted: true },
78
+ { type: "text", text: "Hello" },
79
+ ],
80
+ }),
81
+ ];
82
+ const report = createEmptyReport("anthropic-messages", "openai-responses");
83
+ transformMessages(messages, model, undefined, report);
84
+ assert.equal(report.thinkingBlocksDropped, 1);
85
+ });
86
+
87
+ test("tracks thinking blocks downgraded to plain text", () => {
88
+ const model = makeModel({ id: "gpt-5", api: "openai-responses", provider: "openai" });
89
+ const messages: Message[] = [
90
+ makeAssistantMsg({
91
+ content: [
92
+ { type: "thinking", thinking: "Let me think about this..." },
93
+ { type: "text", text: "Here is my answer" },
94
+ ],
95
+ }),
96
+ ];
97
+ const report = createEmptyReport("anthropic-messages", "openai-responses");
98
+ transformMessages(messages, model, undefined, report);
99
+ assert.equal(report.thinkingBlocksDowngraded, 1);
100
+ });
101
+
102
+ test("tracks tool call IDs remapped", () => {
103
+ const model = makeModel({ id: "claude-sonnet-4-6", api: "anthropic-messages", provider: "anthropic" });
104
+ const toolCall: ToolCall = {
105
+ type: "toolCall",
106
+ id: "original-long-id-that-needs-normalization|with-special-chars",
107
+ name: "bash",
108
+ arguments: { command: "ls" },
109
+ };
110
+ const messages: Message[] = [
111
+ makeAssistantMsg({
112
+ provider: "openai",
113
+ api: "openai-responses",
114
+ model: "gpt-5",
115
+ content: [toolCall],
116
+ }),
117
+ ];
118
+ const normalizer = (id: string) => id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
119
+ const report = createEmptyReport("openai-responses", "anthropic-messages");
120
+ transformMessages(messages, model, normalizer, report);
121
+ assert.equal(report.toolCallIdsRemapped, 1);
122
+ });
123
+
124
+ test("tracks thought signatures dropped", () => {
125
+ const model = makeModel({ id: "claude-sonnet-4-6", api: "anthropic-messages", provider: "anthropic" });
126
+ const toolCall: ToolCall = {
127
+ type: "toolCall",
128
+ id: "tc_001",
129
+ name: "bash",
130
+ arguments: { command: "ls" },
131
+ thoughtSignature: "some-opaque-signature",
132
+ };
133
+ const messages: Message[] = [
134
+ makeAssistantMsg({
135
+ provider: "google",
136
+ api: "google-generative-ai",
137
+ model: "gemini-2.5-pro",
138
+ content: [toolCall],
139
+ }),
140
+ ];
141
+ const report = createEmptyReport("google-generative-ai", "anthropic-messages");
142
+ transformMessages(messages, model, undefined, report);
143
+ assert.equal(report.thoughtSignaturesDropped, 1);
144
+ });
145
+
146
+ test("tracks synthetic tool results inserted", () => {
147
+ const model = makeModel();
148
+ const toolCall: ToolCall = {
149
+ type: "toolCall",
150
+ id: "tc_orphan",
151
+ name: "bash",
152
+ arguments: { command: "ls" },
153
+ };
154
+ // Assistant message with tool call followed by another assistant (no tool result)
155
+ const messages: Message[] = [
156
+ makeAssistantMsg({ content: [toolCall, { type: "text", text: "Using bash" }] }),
157
+ makeAssistantMsg({ content: [{ type: "text", text: "Next message" }] }),
158
+ ];
159
+ const report = createEmptyReport("anthropic-messages", "anthropic-messages");
160
+ transformMessages(messages, model, undefined, report);
161
+ assert.equal(report.syntheticToolResultsInserted, 1);
162
+ });
163
+
164
+ test("does not count transformations for same-model messages", () => {
165
+ const model = makeModel();
166
+ const messages: Message[] = [
167
+ makeAssistantMsg({
168
+ content: [
169
+ { type: "thinking", thinking: "Let me think..." },
170
+ { type: "text", text: "Answer" },
171
+ ],
172
+ }),
173
+ ];
174
+ const report = createEmptyReport("anthropic-messages", "anthropic-messages");
175
+ transformMessages(messages, model, undefined, report);
176
+ assert.equal(report.thinkingBlocksDowngraded, 0);
177
+ assert.equal(report.thinkingBlocksDropped, 0);
178
+ });
179
+
180
+ test("works without report parameter (backward compatible)", () => {
181
+ const model = makeModel();
182
+ const messages: Message[] = [
183
+ makeAssistantMsg({ content: [{ type: "text", text: "Hello" }] }),
184
+ ];
185
+ // Should not throw
186
+ const result = transformMessages(messages, model);
187
+ assert.ok(Array.isArray(result));
188
+ });
189
+ });