Skip to content

Commit d1e58fb

Browse files
committed
feat(api): Add Responses API support for GPT-5.4 and newer models
Implement full OpenAI Responses API streaming for GitHub Copilot models that exclusively support /responses (e.g. GPT-5.4). Six bugs fixed in the initial implementation: - Content type: use input_text/output_text per Responses API spec - Tool parameters: encode as JSON objects via CodableAny wrapper, not base64 strings (JSONSerialization.data -> Codable was base64) - System prompt: include as developer-role messages (was filtered out during token counting and never re-added) - Unsupported parameter: remove top_p from Responses requests - Finish reason: track tool call presence across streaming events and emit "tool_calls" instead of always "stop", so the orchestrator actually executes tools instead of treating them as natural completion - Stream events: handle all 9 Responses API informational event types (response.created, response.in_progress, function_call_arguments.delta, etc.) as .ignored instead of throwing DecodingError warnings API routing: prefer Chat Completions when model supports both endpoints, only use Responses API when model exclusively supports /responses. Added truncation and reasoning configuration to ResponsesRequest. Mermaid diagram improvements: - Bundle mermaid.min.js locally for offline rendering (no CDN dependency) - New MermaidWebRenderer: renders via offscreen WKWebView using bundled engine, respects light/dark theme, proper image sizing - CachedDiagramView rewritten to use MermaidWebRenderer instead of inline WKWebView with CDN script tag - Export/print: diagrams render as high-quality images in PDF, DOCX, and PPTX instead of showing raw mermaid code - MermaidDiagramExporter: dedicated exporter for static image capture using WKNavigationDelegate for reliable completion detection - Expanded system prompt with 20+ diagram types: state, ER, sankey, kanban, block, packet, C4 architecture, and syntax correction notes for commonly misgenerated types (xychart-beta, sankey-beta, etc.) Version: 20260316.2
1 parent b3a80ee commit d1e58fb

24 files changed

Lines changed: 5404 additions & 1171 deletions

Info.plist

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
<key>CFBundlePackageType</key>
2020
<string>APPL</string>
2121
<key>CFBundleShortVersionString</key>
22-
<string>20260316.1</string>
22+
<string>20260316.2</string>
2323
<key>CFBundleVersion</key>
24-
<string>20260316.1</string>
24+
<string>20260316.2</string>
2525
<key>LSApplicationCategoryType</key>
2626
<string>public.app-category.productivity</string>
2727
<key>LSMinimumSystemVersion</key>

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ build-debug: llamacpp
7373
@cp Resources/vendor/xterm/xterm-addon-fit.js .build/Build/Products/Debug/SAM.app/Contents/Resources/
7474
@cp Sources/ConfigurationSystem/Resources/model_config.json .build/Build/Products/Debug/SAM.app/Contents/Resources/
7575
@cp Resources/whats-new.json .build/Build/Products/Debug/SAM.app/Contents/Resources/
76+
@cp Resources/help.json .build/Build/Products/Debug/SAM.app/Contents/Resources/
77+
@cp Resources/vendor/mermaid/mermaid.min.js .build/Build/Products/Debug/SAM.app/Contents/Resources/
7678
@cp -R .build/Build/Products/Debug/PackageFrameworks/llama.framework .build/Build/Products/Debug/SAM.app/Contents/Frameworks/
7779
@if [ -d ".build/Build/Products/Debug/Sparkle.framework" ]; then \
7880
echo "Copying Sparkle.framework to app bundle..."; \
@@ -115,6 +117,8 @@ build-release: llamacpp
115117
@cp Resources/vendor/xterm/xterm-addon-fit.js .build/Build/Products/Release/SAM.app/Contents/Resources/
116118
@cp Sources/ConfigurationSystem/Resources/model_config.json .build/Build/Products/Release/SAM.app/Contents/Resources/
117119
@cp Resources/whats-new.json .build/Build/Products/Release/SAM.app/Contents/Resources/
120+
@cp Resources/help.json .build/Build/Products/Release/SAM.app/Contents/Resources/
121+
@cp Resources/vendor/mermaid/mermaid.min.js .build/Build/Products/Release/SAM.app/Contents/Resources/
118122
@cp -R .build/Build/Products/Release/PackageFrameworks/llama.framework .build/Build/Products/Release/SAM.app/Contents/Frameworks/
119123
@echo "Fixing llama framework install name..."
120124
@install_name_tool -id "@rpath/llama.framework/Versions/A/llama" .build/Build/Products/Release/SAM.app/Contents/Frameworks/llama.framework/Versions/A/llama 2>/dev/null || true

Resources/vendor/mermaid/mermaid.min.js

Lines changed: 3022 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Resources/whats-new.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,50 @@
11
{
22
"releases": [
3+
{
4+
"version": "20260316.2",
5+
"release_date": "March 16, 2026",
6+
"introduction": "This release adds support for the latest GPT models, overhauls diagram rendering for better quality and offline use, and fixes several agent reliability issues.",
7+
"improvements": [
8+
{
9+
"id": "gpt-5-4-support",
10+
"icon": "cpu.fill",
11+
"title": "GPT-5.4 & New Model Support",
12+
"description": "SAM now supports GPT-5.4 and other models that use OpenAI's newer Responses API. These models are automatically detected from your GitHub Copilot subscription and work with all of SAM's tools and agent capabilities."
13+
},
14+
{
15+
"id": "mermaid-rendering",
16+
"icon": "chart.bar.doc.horizontal.fill",
17+
"title": "Improved Diagram Rendering",
18+
"description": "Diagrams now render using a bundled engine instead of loading from the internet, so they appear faster and work offline. Rendering respects your light/dark theme setting, and diagrams are properly sized instead of stretching to fill the window."
19+
},
20+
{
21+
"id": "more-diagram-types",
22+
"icon": "square.grid.3x3.fill",
23+
"title": "More Diagram Types",
24+
"description": "SAM can now generate over 20 diagram types including state diagrams, ER diagrams, Sankey flow charts, Kanban boards, packet diagrams, block diagrams, and C4 architecture diagrams - in addition to the existing flowcharts, sequence diagrams, Gantt charts, pie charts, and more."
25+
},
26+
{
27+
"id": "diagram-in-documents",
28+
"icon": "doc.richtext.fill",
29+
"title": "Diagrams in Exported Documents",
30+
"description": "When you export a conversation as PDF, Word, or PowerPoint, any diagrams are now rendered as high-quality images in the document. Previously, exports would show the raw diagram code instead of the rendered visual."
31+
}
32+
],
33+
"bugfixes": [
34+
{
35+
"id": "tool-execution-responses-api",
36+
"icon": "wrench.and.screwdriver.fill",
37+
"title": "Fixed Agent Tool Execution",
38+
"description": "Fixed a bug where the agent would recognize tool calls but never execute them when using certain models. The agent now correctly runs tools and continues multi-step workflows on all supported models."
39+
},
40+
{
41+
"id": "system-prompt-responses",
42+
"icon": "text.bubble.fill",
43+
"title": "Fixed System Prompt for New Models",
44+
"description": "System prompts and agent instructions are now correctly sent to all models. Previously, some newer models would ignore the system prompt entirely, leading to generic responses without SAM's personality or capabilities."
45+
}
46+
]
47+
},
348
{
449
"version": "20260316.1",
550
"release_date": "March 16, 2026",

Sources/APIFramework/GitHubCopilotResponsesModels.swift

Lines changed: 164 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -167,33 +167,45 @@ public struct ResponsesRequest: Codable {
167167
public let previousResponseId: String?
168168
public let stream: Bool
169169
public let tools: [ResponsesFunctionTool]?
170-
public let topP: Double?
171170
public let maxOutputTokens: Int?
172171
public let toolChoice: String?
173172
public let store: Bool
173+
public let truncation: String?
174+
public let reasoning: ResponsesReasoning?
174175

175176
enum CodingKeys: String, CodingKey {
176177
case model
177178
case input
178179
case previousResponseId = "previous_response_id"
179180
case stream
180181
case tools
181-
case topP = "top_p"
182182
case maxOutputTokens = "max_output_tokens"
183183
case toolChoice = "tool_choice"
184184
case store
185+
case truncation
186+
case reasoning
185187
}
186188

187-
public init(model: String, input: [ResponsesInputItem], previousResponseId: String?, stream: Bool, tools: [ResponsesFunctionTool]?, topP: Double?, maxOutputTokens: Int?, toolChoice: String?, store: Bool = false) {
189+
public init(model: String, input: [ResponsesInputItem], previousResponseId: String?, stream: Bool, tools: [ResponsesFunctionTool]?, maxOutputTokens: Int?, toolChoice: String?, store: Bool = false, truncation: String? = "disabled", reasoning: ResponsesReasoning? = ResponsesReasoning()) {
188190
self.model = model
189191
self.input = input
190192
self.previousResponseId = previousResponseId
191193
self.stream = stream
192194
self.tools = tools
193-
self.topP = topP
194195
self.maxOutputTokens = maxOutputTokens
195196
self.toolChoice = toolChoice
196197
self.store = store
198+
self.truncation = truncation
199+
self.reasoning = reasoning
200+
}
201+
}
202+
203+
/// Responses API reasoning configuration.
204+
public struct ResponsesReasoning: Codable {
205+
public let effort: String
206+
207+
public init(effort: String = "medium") {
208+
self.effort = effort
197209
}
198210
}
199211

@@ -263,7 +275,8 @@ public struct ResponsesInputMessage: Codable {
263275

264276
/// Responses API content item.
265277
public enum ResponsesContentItem: Codable {
266-
case text(ResponsesTextContent)
278+
case inputText(ResponsesInputTextContent)
279+
case outputText(ResponsesOutputTextContent)
267280

268281
enum CodingKeys: String, CodingKey {
269282
case type
@@ -274,9 +287,13 @@ public enum ResponsesContentItem: Codable {
274287
let type = try container.decode(String.self, forKey: .type)
275288

276289
switch type {
277-
case "text":
278-
let text = try ResponsesTextContent(from: decoder)
279-
self = .text(text)
290+
case "input_text":
291+
let text = try ResponsesInputTextContent(from: decoder)
292+
self = .inputText(text)
293+
294+
case "output_text":
295+
let text = try ResponsesOutputTextContent(from: decoder)
296+
self = .outputText(text)
280297

281298
default:
282299
throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown content item type: \(type)")
@@ -285,15 +302,27 @@ public enum ResponsesContentItem: Codable {
285302

286303
public func encode(to encoder: Encoder) throws {
287304
switch self {
288-
case .text(let text):
305+
case .inputText(let text):
306+
try text.encode(to: encoder)
307+
case .outputText(let text):
289308
try text.encode(to: encoder)
290309
}
291310
}
292311
}
293312

294-
/// Responses API text content.
295-
public struct ResponsesTextContent: Codable {
296-
public var type: String = "text"
313+
/// Responses API input text content (for user/developer messages).
314+
public struct ResponsesInputTextContent: Codable {
315+
public var type: String = "input_text"
316+
public let text: String
317+
318+
public init(text: String) {
319+
self.text = text
320+
}
321+
}
322+
323+
/// Responses API output text content (for assistant messages).
324+
public struct ResponsesOutputTextContent: Codable {
325+
public var type: String = "output_text"
297326
public let text: String
298327

299328
public init(text: String) {
@@ -345,7 +374,8 @@ public struct ResponsesFunctionTool: Codable {
345374
public var type: String = "function"
346375
public let name: String
347376
public let description: String
348-
public let parameters: [String: Any]
377+
/// Parameters stored as raw JSON data to preserve structure during encoding.
378+
public let parametersData: Data
349379
public let strict: Bool = false
350380

351381
enum CodingKeys: String, CodingKey {
@@ -355,20 +385,18 @@ public struct ResponsesFunctionTool: Codable {
355385
public init(name: String, description: String, parameters: [String: Any]) {
356386
self.name = name
357387
self.description = description
358-
self.parameters = parameters
388+
self.parametersData = (try? JSONSerialization.data(withJSONObject: parameters)) ?? Data()
359389
}
360390

361391
public init(from decoder: Decoder) throws {
362392
let container = try decoder.container(keyedBy: CodingKeys.self)
363393
self.name = try container.decode(String.self, forKey: .name)
364394
self.description = try container.decode(String.self, forKey: .description)
365-
366-
/// Decode parameters as JSON object.
367-
let parametersData = try container.decode(Data.self, forKey: .parameters)
368-
if let parametersDict = try JSONSerialization.jsonObject(with: parametersData) as? [String: Any] {
369-
self.parameters = parametersDict
395+
/// Decode parameters however they come.
396+
if let rawJSON = try? container.decode(RawJSON.self, forKey: .parameters) {
397+
self.parametersData = rawJSON.data
370398
} else {
371-
self.parameters = [:]
399+
self.parametersData = Data()
372400
}
373401
}
374402

@@ -379,9 +407,104 @@ public struct ResponsesFunctionTool: Codable {
379407
try container.encode(description, forKey: .description)
380408
try container.encode(strict, forKey: .strict)
381409

382-
/// Encode parameters as JSON.
383-
let parametersData = try JSONSerialization.data(withJSONObject: parameters)
384-
try container.encode(parametersData, forKey: .parameters)
410+
/// Encode parameters as a raw JSON object (not as a string).
411+
let rawJSON = RawJSON(data: parametersData)
412+
try container.encode(rawJSON, forKey: .parameters)
413+
}
414+
}
415+
416+
/// Helper type that preserves raw JSON structure through Codable encoding/decoding.
417+
/// Encodes as a native JSON object/array/value rather than a string or base64.
418+
private struct RawJSON: Codable {
419+
let data: Data
420+
421+
init(data: Data) {
422+
self.data = data
423+
}
424+
425+
init(from decoder: Decoder) throws {
426+
let container = try decoder.singleValueContainer()
427+
/// Try decoding as various JSON types and re-serialize.
428+
if let dict = try? container.decode([String: RawJSON].self) {
429+
let rebuilt = dict.mapValues { (try? JSONSerialization.jsonObject(with: $0.data)) ?? NSNull() }
430+
self.data = (try? JSONSerialization.data(withJSONObject: rebuilt)) ?? Data()
431+
} else if let string = try? container.decode(String.self) {
432+
/// Maybe it's a JSON string - try parsing it.
433+
if let parsed = string.data(using: .utf8),
434+
(try? JSONSerialization.jsonObject(with: parsed)) != nil {
435+
self.data = parsed
436+
} else {
437+
self.data = (try? JSONSerialization.data(withJSONObject: string)) ?? Data()
438+
}
439+
} else {
440+
self.data = Data()
441+
}
442+
}
443+
444+
func encode(to encoder: Encoder) throws {
445+
var container = encoder.singleValueContainer()
446+
guard !data.isEmpty,
447+
let jsonObject = try? JSONSerialization.jsonObject(with: data) else {
448+
try container.encode([String: String]())
449+
return
450+
}
451+
/// Re-encode via a CodableAny wrapper.
452+
let wrapped = CodableAny(jsonObject)
453+
try container.encode(wrapped)
454+
}
455+
}
456+
457+
/// Wraps Any JSON value for Codable encoding.
458+
private enum CodableAny: Codable {
459+
case string(String)
460+
case int(Int)
461+
case double(Double)
462+
case bool(Bool)
463+
case null
464+
case array([CodableAny])
465+
case object([String: CodableAny])
466+
467+
init(_ value: Any) {
468+
if let s = value as? String { self = .string(s) }
469+
else if let n = value as? NSNumber {
470+
// Check for boolean before numeric (NSNumber bridges both)
471+
if CFGetTypeID(n) == CFBooleanGetTypeID() {
472+
self = .bool(n.boolValue)
473+
} else if n.doubleValue == Double(n.intValue) {
474+
self = .int(n.intValue)
475+
} else {
476+
self = .double(n.doubleValue)
477+
}
478+
}
479+
else if let a = value as? [Any] { self = .array(a.map { CodableAny($0) }) }
480+
else if let d = value as? [String: Any] { self = .object(d.mapValues { CodableAny($0) }) }
481+
else if value is NSNull { self = .null }
482+
else { self = .null }
483+
}
484+
485+
init(from decoder: Decoder) throws {
486+
let container = try decoder.singleValueContainer()
487+
if let s = try? container.decode(String.self) { self = .string(s) }
488+
else if let i = try? container.decode(Int.self) { self = .int(i) }
489+
else if let d = try? container.decode(Double.self) { self = .double(d) }
490+
else if let b = try? container.decode(Bool.self) { self = .bool(b) }
491+
else if let a = try? container.decode([CodableAny].self) { self = .array(a) }
492+
else if let o = try? container.decode([String: CodableAny].self) { self = .object(o) }
493+
else if container.decodeNil() { self = .null }
494+
else { self = .null }
495+
}
496+
497+
func encode(to encoder: Encoder) throws {
498+
var container = encoder.singleValueContainer()
499+
switch self {
500+
case .string(let s): try container.encode(s)
501+
case .int(let i): try container.encode(i)
502+
case .double(let d): try container.encode(d)
503+
case .bool(let b): try container.encode(b)
504+
case .null: try container.encodeNil()
505+
case .array(let a): try container.encode(a)
506+
case .object(let o): try container.encode(o)
507+
}
385508
}
386509
}
387510

@@ -394,6 +517,7 @@ public enum ResponsesStreamEvent: Codable {
394517
case outputItemAdded(ResponsesOutputItemAddedEvent)
395518
case outputItemDone(ResponsesOutputItemDoneEvent)
396519
case completed(ResponsesCompletedEvent)
520+
case ignored
397521

398522
enum CodingKeys: String, CodingKey {
399523
case type
@@ -424,8 +548,21 @@ public enum ResponsesStreamEvent: Codable {
424548
let event = try ResponsesCompletedEvent(from: decoder)
425549
self = .completed(event)
426550

551+
case "response.created",
552+
"response.in_progress",
553+
"response.content_part.added",
554+
"response.content_part.done",
555+
"response.output_text.done",
556+
"response.function_call_arguments.delta",
557+
"response.function_call_arguments.done",
558+
"response.reasoning_summary_text.delta",
559+
"response.reasoning_summary_text.done":
560+
/// Informational events - safely ignored.
561+
/// Tool call args come via outputItemDone with complete data.
562+
self = .ignored
563+
427564
default:
428-
/// Ignore unknown event types.
565+
/// Truly unknown event types.
429566
throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown event type: \(type)")
430567
}
431568
}
@@ -446,6 +583,9 @@ public enum ResponsesStreamEvent: Codable {
446583

447584
case .completed(let event):
448585
try event.encode(to: encoder)
586+
587+
case .ignored:
588+
break
449589
}
450590
}
451591
}

0 commit comments

Comments
 (0)