Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Fluid.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
7C3697892ED70F9C005874CE /* DynamicNotchKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7C3697882ED70F9C005874CE /* DynamicNotchKit */; };
7C5AF14B2F15041600DE21B0 /* MediaRemoteAdapter in Frameworks */ = {isa = PBXBuildFile; productRef = 7C5AF14A2F15041600DE21B0 /* MediaRemoteAdapter */; };
7C91B0012F42AA0100C0DEF0 /* HotkeyShortcutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C91B0022F42AA0100C0DEF0 /* HotkeyShortcutTests.swift */; };
9E1636273C4735E04F0940B0 /* LLMClientThinkingTagTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FD426ADEB23850ABB2FFEA /* LLMClientThinkingTagTests.swift */; };
7CDB0A2D2F3C4D5600FB7CAD /* DictationE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CDB0A292F3C4D5600FB7CAD /* DictationE2ETests.swift */; };
7CDB0A2E2F3C4D5600FB7CAD /* AudioFixtureLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CDB0A2A2F3C4D5600FB7CAD /* AudioFixtureLoader.swift */; };
7CDB0A2F2F3C4D5600FB7CAD /* dictation_fixture.wav in Resources */ = {isa = PBXBuildFile; fileRef = 7CDB0A2B2F3C4D5600FB7CAD /* dictation_fixture.wav */; };
Expand All @@ -33,6 +34,7 @@
7C078D8F2E3B339200FB7CAC /* FluidVoice Debug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "FluidVoice Debug.app"; sourceTree = BUILT_PRODUCTS_DIR; };
7CDB0A202F3C4D5600FB7CAD /* FluidDictationIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FluidDictationIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
7C91B0022F42AA0100C0DEF0 /* HotkeyShortcutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyShortcutTests.swift; sourceTree = "<group>"; };
18FD426ADEB23850ABB2FFEA /* LLMClientThinkingTagTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMClientThinkingTagTests.swift; sourceTree = "<group>"; };
7CDB0A292F3C4D5600FB7CAD /* DictationE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictationE2ETests.swift; sourceTree = "<group>"; };
7CDB0A2A2F3C4D5600FB7CAD /* AudioFixtureLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioFixtureLoader.swift; sourceTree = "<group>"; };
7CDB0A2B2F3C4D5600FB7CAD /* dictation_fixture.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = dictation_fixture.wav; sourceTree = "<group>"; };
Expand Down Expand Up @@ -104,6 +106,7 @@
7CDB0A272F3C4D5600FB7CAD /* Resources */,
7CDB0A292F3C4D5600FB7CAD /* DictationE2ETests.swift */,
7C91B0022F42AA0100C0DEF0 /* HotkeyShortcutTests.swift */,
18FD426ADEB23850ABB2FFEA /* LLMClientThinkingTagTests.swift */,
);
path = FluidDictationIntegrationTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -258,6 +261,7 @@
7CDB0A2E2F3C4D5600FB7CAD /* AudioFixtureLoader.swift in Sources */,
7CDB0A2D2F3C4D5600FB7CAD /* DictationE2ETests.swift in Sources */,
7C91B0012F42AA0100C0DEF0 /* HotkeyShortcutTests.swift in Sources */,
9E1636273C4735E04F0940B0 /* LLMClientThinkingTagTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
8 changes: 4 additions & 4 deletions Sources/Fluid/Services/LLMClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -795,7 +795,7 @@ final class LLMClient {

// MARK: - Parse Non-Streaming Message

private func parseResponsesResponse(_ json: [String: Any]) throws -> Response {
func parseResponsesResponse(_ json: [String: Any]) throws -> Response {
guard let output = json["output"] as? [[String: Any]] else {
throw LLMError.invalidResponse
}
Expand Down Expand Up @@ -838,12 +838,12 @@ final class LLMClient {

return Response(
thinking: thinking.isEmpty ? nil : thinking,
content: cleanedContent.isEmpty ? rawContent.trimmingCharacters(in: .whitespacesAndNewlines) : cleanedContent,
content: cleanedContent,
toolCalls: parsedToolCalls
)
}

private func parseMessageResponse(_ message: [String: Any]) -> Response {
func parseMessageResponse(_ message: [String: Any]) -> Response {
// Extract content
let rawContent = message["content"] as? String ?? ""

Expand Down Expand Up @@ -878,7 +878,7 @@ final class LLMClient {

return Response(
thinking: finalThinking.isEmpty ? nil : finalThinking,
content: cleanedContent.isEmpty ? rawContent : cleanedContent,
content: cleanedContent,
toolCalls: parsedToolCalls
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
@testable import FluidVoice_Debug
import Foundation
import XCTest

@MainActor
final class LLMClientThinkingTagTests: XCTestCase {
private let client = LLMClient.shared

// MARK: - Chat-completions message parser

func testMessageResponseDropsContentThatIsAllThinking() {
let message: [String: Any] = ["content": "<think>reasoning</think>"]

let response = self.client.parseMessageResponse(message)

XCTAssertTrue(response.content.isEmpty, "All-thinking replies must not leak raw <think> content downstream")
XCTAssertEqual(response.thinking, "reasoning")
}

func testMessageResponseKeepsContentAfterThinking() {
let message: [String: Any] = ["content": "<think>reasoning</think>Hello there"]

let response = self.client.parseMessageResponse(message)

XCTAssertEqual(response.content, "Hello there")
XCTAssertEqual(response.thinking, "reasoning")
}

func testMessageResponseLeavesPlainContentUnchanged() {
let message: [String: Any] = ["content": "Hello there"]

let response = self.client.parseMessageResponse(message)

XCTAssertEqual(response.content, "Hello there")
XCTAssertNil(response.thinking)
}

// MARK: - Responses API parser

func testResponsesResponseDropsContentThatIsAllThinking() throws {
let json = self.responsesPayload(text: "<think>reasoning</think>")

let response = try self.client.parseResponsesResponse(json)

XCTAssertTrue(response.content.isEmpty, "All-thinking replies must not leak raw <think> content downstream")
XCTAssertEqual(response.thinking, "reasoning")
}

func testResponsesResponseKeepsContentAfterThinking() throws {
let json = self.responsesPayload(text: "<think>reasoning</think>Hello there")

let response = try self.client.parseResponsesResponse(json)

XCTAssertEqual(response.content, "Hello there")
XCTAssertEqual(response.thinking, "reasoning")
}

// MARK: - Helpers

private func responsesPayload(text: String) -> [String: Any] {
let part: [String: Any] = ["type": "output_text", "text": text]
let message: [String: Any] = ["type": "message", "content": [part]]
return ["output": [message]]
}
}
Loading