Files

242 lines
8.1 KiB
Swift
Raw Permalink Normal View History

// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#if compiler(>=6.4)
import Foundation
import FoundationModelsFake
import Testing
// MARK: - Test Support Types
private struct EchoFallbackModel: LanguageModel {
struct Executor: LanguageModelExecutor {
struct Configuration: Hashable, Sendable {}
init(configuration: Configuration) throws {}
nonisolated(nonsending) func respond(
to request: LanguageModelExecutorGenerationRequest,
model: EchoFallbackModel,
streamingInto channel: LanguageModelExecutorGenerationChannel
) async throws {
await channel.send(
.response(
entryID: "fallback-response",
action: .appendText("fallback-ok", tokenCount: 2)
)
)
}
}
var capabilities: LanguageModelCapabilities {
LanguageModelCapabilities([.guidedGeneration, .toolCalling, .reasoning])
}
var executorConfiguration: Executor.Configuration {
Executor.Configuration()
}
}
private struct TokenCountTestTool: Tool {
@Generable
struct Arguments {
var query: String
}
var name: String { "search_docs" }
var description: String { "Searches local documentation by query string." }
func call(arguments: Arguments) async throws -> String {
arguments.query
}
}
private struct TestDynamicInstructions: DynamicInstructions {
var body: some DynamicInstructions {
Instructions("Dynamic instructions using default SystemLanguageModel.")
}
}
private struct SystemOrFallbackProfile: LanguageModelSession.DynamicProfile {
let forceFallback: Bool
var body: some LanguageModelSession.DynamicProfile {
if !forceFallback && SystemLanguageModel.default.isAvailable {
Profile {
Instructions("Use the on-device system model.")
}
.model(SystemLanguageModel.default)
} else {
Profile {
Instructions("Use the cross-platform fallback model.")
}
.model(EchoFallbackModel())
}
}
}
// MARK: - Tests
@Suite("SystemLanguageModel Tests")
struct SystemLanguageModelTests {
@Test
func defaultModelPropertiesAndCapabilities() {
let model = SystemLanguageModel.default
let capabilities = model.capabilities
let contextSize = model.contextSize
let variant = model.variant
let availability = model.availability
let isAvailable = model.isAvailable
let supportedLanguages = model.supportedLanguages
#expect(capabilities.contains(.guidedGeneration))
#expect(capabilities.contains(.toolCalling))
#expect(capabilities.contains(.reasoning))
#expect(!capabilities.contains(.vision))
#expect(contextSize == 4096)
#expect(variant == .core3)
#expect(variant.displayName == "Core 3B")
#expect(SystemLanguageModel.Variant.coreAdvanced3.displayName == "Core Advanced 3B")
#expect(availability == .unavailable(.deviceNotEligible))
#expect(!isAvailable)
#expect(supportedLanguages.isEmpty)
#expect(!model.supportsLocale(Locale(identifier: "en_US")))
}
@Test
func useCaseAndGuardrailsConfigurations() {
let generalDefault = SystemLanguageModel(useCase: .general, guardrails: .default)
let taggingPermissive = SystemLanguageModel(
useCase: .contentTagging,
guardrails: .permissiveContentTransformations
)
let configA = generalDefault.executorConfiguration
let configB = taggingPermissive.executorConfiguration
#expect(SystemLanguageModel.UseCase.general != .contentTagging)
#expect(configA != configB)
}
@Test
func availabilityEquatableAndErrorDescriptions() {
let availabilities: [SystemLanguageModel.Availability] = [
.available,
.unavailable(.deviceNotEligible),
.unavailable(.appleIntelligenceNotEnabled),
.unavailable(.modelNotReady),
]
let error = SystemLanguageModel.Error.assetsUnavailable(.init(debugDescription: "Missing"))
let distinctPairs = availabilities.indices.flatMap { lhs in
availabilities.indices.filter { $0 != lhs && availabilities[$0] == availabilities[lhs] }
}
#expect(distinctPairs.isEmpty)
#expect(error.errorDescription == "Missing")
#expect(error.debugDescription == "Missing")
}
@Test
func tokenCountOverloads() async throws {
let model = SystemLanguageModel.default
let prompt = Prompt("Count the tokens in this prompt sentence.")
let instructions = Instructions("You are a concise assistant.")
let tools: [any Tool] = [TokenCountTestTool()]
let schema = TokenCountTestTool.Arguments.generationSchema
let entries: [Transcript.Entry] = [
.instructions(
Transcript.Instructions(
segments: [.text(.init(content: "You are a concise assistant."))],
toolDefinitions: [Transcript.ToolDefinition(tool: TokenCountTestTool())]
)
),
.prompt(
Transcript.Prompt(
segments: [.text(.init(content: "Count the tokens in this prompt sentence."))]
)
),
]
let promptTokens = try await model.tokenCount(for: prompt)
let instructionTokens = try await model.tokenCount(for: instructions)
let toolTokens = try await model.tokenCount(for: tools)
let schemaTokens = try await model.tokenCount(for: schema)
let entryTokens = try await model.tokenCount(for: entries)
let emptyToolTokens = try await model.tokenCount(for: [] as [any Tool])
#expect(promptTokens > 0)
#expect(instructionTokens > 0)
#expect(toolTokens > 0)
#expect(schemaTokens > 0)
#expect(entryTokens >= promptTokens + instructionTokens)
#expect(emptyToolTokens == 0)
}
@Test
func sessionDefaultModelInitializers() {
let bareSession = LanguageModelSession()
let stringSession = LanguageModelSession(instructions: "Be helpful.")
let builderSession = LanguageModelSession {
"Built instructions."
}
let instructionsSession = LanguageModelSession(instructions: Instructions("Explicit."))
let transcriptSession = LanguageModelSession(transcript: Transcript())
let dynamicSession = LanguageModelSession(
dynamicInstructions: TestDynamicInstructions()
)
let counts = [
bareSession.transcript.count,
stringSession.transcript.count,
builderSession.transcript.count,
instructionsSession.transcript.count,
transcriptSession.transcript.count,
dynamicSession.transcript.count,
]
#expect(counts == [0, 1, 1, 1, 0, 1])
}
@Test
func dynamicProfileBranchingBetweenSystemAndFallbackModel() async throws {
let forcedSession = LanguageModelSession(
profile: SystemOrFallbackProfile(forceFallback: true))
let unforcedSession = LanguageModelSession(
profile: SystemOrFallbackProfile(forceFallback: false)
)
let forcedResponse = try await forcedSession.respond(to: "Ping")
let unforcedResponse = try await unforcedSession.respond(to: "Ping")
#expect(forcedResponse.content == "fallback-ok")
#expect(forcedSession.transcript.count == 3)
#expect(unforcedResponse.content == "fallback-ok")
}
@Test
func respondThrowsAssetsUnavailable() async throws {
let session = LanguageModelSession(
model: SystemLanguageModel.default,
tools: [TokenCountTestTool()],
instructions: "Reply in one short sentence."
)
await #expect(throws: SystemLanguageModel.Error.self) {
_ = try await session.respond(to: "Say hello.")
}
}
}
#endif // compiler(>=6.4)