mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-09 22:36:01 +05:45
242 lines
8.1 KiB
Swift
242 lines
8.1 KiB
Swift
// 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)
|