From c82e5d081a345dcb95f71efa112896c706a94759 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Thu, 10 Sep 2026 17:42:47 -0400 Subject: [PATCH 1/4] feat(ai): add Gemini Interactions API support Add support for the Gemini Interactions API alongside the existing Generate Content API in GeminiLanguageModel. * Extract shared data models (`JSONValue` and `GoogleCloudAPIError`) into `GeminiSharedDataModels`. * Add `InteractionsDataModels` containing Swift types generated from the Gemini Interactions OpenAPI specification. * Implement Interactions URL routing and SSE streaming in `GeminiAPIClient`. * Introduce `GeminiLanguageModel.APIVariant` to toggle between Generate Content and Interactions API backends. * Implement transcript and request translators for the Interactions API and branch executor execution accordingly. * Parameterize all integration tests across supported backends and API variants, gracefully cancelling Interactions tests for Firebase AI Logic until proxy support is available. --- GeminiLanguageModel/Package.swift | 26 +- .../GeminiAPIClient/GeminiAPIClient.swift | 263 ++++++++++++ .../GeminiAPIClient/GeminiAPIError.swift | 1 + .../GeminiAPIClient/HTTPLineDecoder.swift | 11 +- .../GeminiAPIClient/ModelResource.swift | 6 +- .../GenerateContent/CountTokensRequest.swift | 1 + .../GenerateContent/FunctionCall.swift | 1 + .../GenerateContent/FunctionDeclaration.swift | 1 + .../GenerateContent/FunctionResponse.swift | 1 + .../GenerateContent/GenerationConfig.swift | 1 + .../GenerateContent/Part.swift | 1 + .../GenerateContent/Schema.swift | 1 + .../GenerateContent/TemplateFunction.swift | 1 + .../TemplateGenerateContentRequest.swift | 1 + .../GenerateContent/TextResponseFormat.swift | 1 + .../GenerateContent/ToolCall.swift | 1 + .../GenerateContent/ToolResponse.swift | 1 + .../GeminiErrorMapper.swift | 1 + .../GeminiInteractionsRequestTranslator.swift | 122 ++++++ ...miniInteractionsTranscriptTranslator.swift | 210 ++++++++++ .../GeminiLanguageModel+Error.swift | 14 - .../GeminiLanguageModel.swift | 24 +- .../GeminiLanguageModelExecutor.swift | 228 ++++++++++- .../GeminiRequestTranslator.swift | 1 + .../GeminiTranscriptTranslator.swift | 1 + .../GenerationSchema+Gemini.swift | 1 + .../GoogleCloudAPIError.swift | 0 .../JSONValue.swift | 0 .../InteractionsDataModels/Agent.swift | 86 ++++ .../InteractionsDataModels/AgentOption.swift | 58 +++ .../InteractionsDataModels/AgentTool.swift | 22 + .../AllowedTools+Mode.swift | 60 +++ .../InteractionsDataModels/AllowedTools.swift | 40 ++ .../AllowlistEntry.swift | 42 ++ .../InteractionsDataModels/Annotation.swift | 22 + .../AntigravityAgentConfig.swift | 46 +++ .../ArgumentsDelta.swift | 36 ++ .../AudioContent+MimeType.swift | 105 +++++ .../InteractionsDataModels/AudioContent.swift | 67 ++++ .../AudioDelta+MimeType.swift | 104 +++++ .../InteractionsDataModels/AudioDelta.swift | 72 ++++ .../AudioResponseFormat+Delivery.swift | 50 +++ .../AudioResponseFormat+MimeType.swift | 70 ++++ .../AudioResponseFormat.swift | 59 +++ .../CodeExecution.swift | 28 ++ .../CodeExecutionCallArguments+Language.swift | 45 +++ .../CodeExecutionCallArguments.swift | 38 ++ .../CodeExecutionCallDelta.swift | 45 +++ .../CodeExecutionCallStep.swift | 53 +++ .../CodeExecutionResultDelta.swift | 51 +++ .../CodeExecutionResultStep.swift | 60 +++ .../ComputerUse+Environment.swift | 55 +++ .../InteractionsDataModels/ComputerUse.swift | 59 +++ .../InteractionsDataModels/Content.swift | 83 ++++ .../CreateModelInteraction+Status.swift | 81 ++++ .../CreateModelInteraction.swift | 187 +++++++++ ...eepResearchAgentConfig+Visualization.swift | 50 +++ .../DeepResearchAgentConfig.swift | 54 +++ .../DocumentContent+MimeType.swift | 50 +++ .../DocumentContent.swift | 53 +++ .../DocumentDelta+MimeType.swift | 49 +++ .../DocumentDelta.swift | 50 +++ .../DynamicAgentConfig.swift | 28 ++ .../InteractionsDataModels/Empty.swift | 28 ++ .../InteractionsDataModels/Environment.swift | 53 +++ .../EnvironmentNetworkEgressAllowlist.swift | 59 +++ .../InteractionsDataModels/Error.swift | 40 ++ .../InteractionsDataModels/ErrorEvent.swift | 44 ++ .../InteractionsDataModels/FileCitation.swift | 90 +++++ .../InteractionsDataModels/FileSearch.swift | 51 +++ .../FileSearchCallDelta.swift | 39 ++ .../FileSearchCallStep.swift | 46 +++ .../FileSearchResult.swift | 22 + .../FileSearchResultDelta.swift | 45 +++ .../FileSearchResultStep.swift | 46 +++ .../InteractionsDataModels/Function.swift | 53 +++ .../FunctionCallStep.swift | 53 +++ .../FunctionResultDelta.swift | 57 +++ .../FunctionResultStep.swift | 60 +++ .../GenerationConfig.swift | 100 +++++ .../InteractionsDataModels/GoogleMaps.swift | 52 +++ .../GoogleMapsCallArguments.swift | 33 ++ .../GoogleMapsCallDelta.swift | 46 +++ .../GoogleMapsCallStep.swift | 53 +++ .../GoogleMapsResult.swift | 38 ++ .../GoogleMapsResultDelta.swift | 46 +++ .../GoogleMapsResultPlaces.swift | 50 +++ .../GoogleMapsResultStep.swift | 52 +++ .../InteractionsDataModels/GoogleSearch.swift | 37 ++ .../GoogleSearchCallArguments.swift | 33 ++ .../GoogleSearchCallDelta.swift | 45 +++ .../GoogleSearchCallStep+SearchType.swift | 50 +++ .../GoogleSearchCallStep.swift | 60 +++ .../GoogleSearchResult.swift | 33 ++ .../GoogleSearchResultDelta.swift | 51 +++ .../GoogleSearchResultStep.swift | 60 +++ .../GroundingToolCount+Type.swift | 51 +++ .../GroundingToolCount.swift | 40 ++ .../InteractionsDataModels/HarmCategory.swift | 90 +++++ .../ImageConfig+AspectRatio.swift | 109 +++++ .../ImageConfig+ImageSize.swift | 59 +++ .../InteractionsDataModels/ImageConfig.swift | 39 ++ .../ImageContent+MimeType.swift | 80 ++++ .../InteractionsDataModels/ImageContent.swift | 60 +++ .../ImageDelta+MimeType.swift | 79 ++++ .../InteractionsDataModels/ImageDelta.swift | 57 +++ .../ImageResponseFormat+AspectRatio.swift | 110 +++++ .../ImageResponseFormat+Delivery.swift | 50 +++ .../ImageResponseFormat+ImageSize.swift | 60 +++ .../ImageResponseFormat+MimeType.swift | 45 +++ .../ImageResponseFormat.swift | 56 +++ .../Interaction+Status.swift | 81 ++++ .../InteractionsDataModels/Interaction.swift | 201 ++++++++++ .../InteractionCompletedEvent.swift | 45 +++ .../InteractionCreatedEvent.swift | 45 +++ .../InteractionSSEEvent.swift | 102 +++++ .../InteractionSSEStreamEvent.swift | 32 ++ ...nteractionSseEventInteraction+Status.swift | 70 ++++ .../InteractionSseEventInteraction.swift | 98 +++++ .../InteractionStatusUpdate+Status.swift | 75 ++++ .../InteractionStatusUpdate.swift | 50 +++ .../InteractionsInput.swift | 65 +++ .../InteractionsDataModels/MCPServer.swift | 59 +++ .../MCPServerToolCallDelta.swift | 50 +++ .../MCPServerToolCallStep.swift | 60 +++ .../MCPServerToolResultDelta.swift | 50 +++ .../MCPServerToolResultStep.swift | 60 +++ .../MediaProcessing.swift | 22 + .../MediaResolution.swift | 58 +++ .../ModalityTokens.swift | 40 ++ .../InteractionsDataModels/Model.swift | 153 +++++++ .../ModelOutputStep.swift | 36 ++ .../PlaceCitation.swift | 75 ++++ .../ProcessingCallDelta.swift | 39 ++ .../ProcessingCallStep.swift | 47 +++ .../ProcessingResultDelta.swift | 39 ++ .../ProcessingResultStep.swift | 46 +++ .../ResponseFormat.swift | 72 ++++ .../ResponseModality.swift | 63 +++ .../ReviewSnippet.swift | 48 +++ .../SafetySetting+Method.swift | 51 +++ .../SafetySetting+Threshold.swift | 66 +++ .../SafetySetting.swift | 54 +++ .../InteractionsDataModels/ServiceTier.swift | 53 +++ .../SmartTranscriptionMode.swift | 28 ++ .../InteractionsDataModels/Source+Type.swift | 55 +++ .../InteractionsDataModels/Source.swift | 62 +++ .../SpeakerConfig.swift | 33 ++ .../InteractionsDataModels/SpeechConfig.swift | 47 +++ .../StaticMediaProcessing.swift | 54 +++ .../Sources/InteractionsDataModels/Step.swift | 196 +++++++++ .../InteractionsDataModels/StepDelta.swift | 56 +++ .../StepDeltaData.swift | 235 +++++++++++ .../StepDeltaMetadata.swift | 33 ++ .../InteractionsDataModels/StepStart.swift | 50 +++ .../InteractionsDataModels/StepStop.swift | 58 +++ .../TextAnnotationDelta.swift | 37 ++ .../InteractionsDataModels/TextContent.swift | 44 ++ .../InteractionsDataModels/TextDelta.swift | 36 ++ .../TextResponseFormat+MimeType.swift | 50 +++ .../TextResponseFormat.swift | 47 +++ .../ThinkingLevel.swift | 58 +++ .../ThinkingSummaries.swift | 48 +++ .../ThoughtSignatureDelta.swift | 39 ++ .../InteractionsDataModels/ThoughtStep.swift | 46 +++ .../ThoughtSummaryContent.swift | 59 +++ .../ThoughtSummaryDelta.swift | 37 ++ .../Sources/InteractionsDataModels/Tool.swift | 107 +++++ .../ToolChoiceConfig.swift | 33 ++ .../TranscriptionConfig.swift | 76 ++++ .../TranscriptionMode.swift | 22 + .../InteractionsDataModels/URLCitation.swift | 60 +++ .../InteractionsDataModels/URLContext.swift | 28 ++ .../URLContextCallArguments.swift | 33 ++ .../URLContextCallDelta.swift | 45 +++ .../URLContextCallStep.swift | 53 +++ .../URLContextResult+Status.swift | 60 +++ .../URLContextResult.swift | 40 ++ .../URLContextResultDelta.swift | 51 +++ .../URLContextResultStep.swift | 60 +++ .../InteractionsDataModels/Usage.swift | 104 +++++ .../UserInputStep.swift | 36 ++ .../VerbatimTranscriptionMode.swift | 45 +++ .../VideoConfig+Task.swift | 69 ++++ .../InteractionsDataModels/VideoConfig.swift | 35 ++ .../VideoContent+MimeType.swift | 85 ++++ .../InteractionsDataModels/VideoContent.swift | 76 ++++ .../VideoDelta+MimeType.swift | 89 +++++ .../InteractionsDataModels/VideoDelta.swift | 57 +++ .../VideoResponseFormat+AspectRatio.swift | 50 +++ .../VideoResponseFormat+Delivery.swift | 50 +++ .../VideoResponseFormat+Resolution.swift | 60 +++ .../VideoResponseFormat.swift | 58 +++ .../WebhookConfig.swift | 44 ++ .../InteractionsDataModels/WordInfo.swift | 78 ++++ .../GeminiAPIClientIntegrationTests.swift | 1 + .../GeminiAPIClientTests.swift | 215 ++++++++++ .../GeminiAPIErrorTests.swift | 1 + .../GeminiErrorMapperTests.swift | 1 + ...niInteractionsRequestTranslatorTests.swift | 189 +++++++++ ...nteractionsTranscriptTranslatorTests.swift | 205 ++++++++++ ...GeminiLanguageModelInteractionsTests.swift | 377 ++++++++++++++++++ .../GeminiLanguageModelTests.swift | 1 + .../GeminiRequestTranslatorTests.swift | 1 + .../GeminiTranscriptTranslatorTests.swift | 1 + .../GenerationSchema+GeminiTests.swift | 1 + ...sicContentGenerationIntegrationTests.swift | 30 +- .../GuidedGenerationIntegrationTests.swift | 49 ++- ...onTestingBackend+GeminiLanguageModel.swift | 32 +- .../IntegrationTests/README.md | 29 +- .../ToolCallingIntegrationTests.swift | 59 ++- 211 files changed, 11817 insertions(+), 81 deletions(-) create mode 100644 GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiInteractionsRequestTranslator.swift create mode 100644 GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiInteractionsTranscriptTranslator.swift rename GeminiLanguageModel/Sources/{GeminiAPIDataModels/Shared => GeminiSharedDataModels}/GoogleCloudAPIError.swift (100%) rename GeminiLanguageModel/Sources/{GeminiAPIDataModels/Shared => GeminiSharedDataModels}/JSONValue.swift (100%) create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/Agent.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/AgentOption.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/AgentTool.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/AllowedTools+Mode.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/AllowedTools.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/AllowlistEntry.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/Annotation.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/AntigravityAgentConfig.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ArgumentsDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/AudioContent+MimeType.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/AudioContent.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/AudioDelta+MimeType.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/AudioDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/AudioResponseFormat+Delivery.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/AudioResponseFormat+MimeType.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/AudioResponseFormat.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecution.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionCallArguments+Language.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionCallArguments.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionCallDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionCallStep.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionResultDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionResultStep.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ComputerUse+Environment.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ComputerUse.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/Content.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/CreateModelInteraction+Status.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/CreateModelInteraction.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/DeepResearchAgentConfig+Visualization.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/DeepResearchAgentConfig.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/DocumentContent+MimeType.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/DocumentContent.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/DocumentDelta+MimeType.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/DocumentDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/DynamicAgentConfig.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/Empty.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/Environment.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/EnvironmentNetworkEgressAllowlist.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/Error.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ErrorEvent.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/FileCitation.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/FileSearch.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchCallDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchCallStep.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchResult.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchResultDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchResultStep.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/Function.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/FunctionCallStep.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/FunctionResultDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/FunctionResultStep.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/GenerationConfig.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMaps.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsCallArguments.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsCallDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsCallStep.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsResult.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsResultDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsResultPlaces.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsResultStep.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearch.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchCallArguments.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchCallDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchCallStep+SearchType.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchCallStep.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchResult.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchResultDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchResultStep.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/GroundingToolCount+Type.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/GroundingToolCount.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/HarmCategory.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ImageConfig+AspectRatio.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ImageConfig+ImageSize.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ImageConfig.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ImageContent+MimeType.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ImageContent.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ImageDelta+MimeType.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ImageDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat+AspectRatio.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat+Delivery.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat+ImageSize.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat+MimeType.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/Interaction+Status.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/Interaction.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/InteractionCompletedEvent.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/InteractionCreatedEvent.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/InteractionSSEEvent.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/InteractionSSEStreamEvent.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/InteractionSseEventInteraction+Status.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/InteractionSseEventInteraction.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/InteractionStatusUpdate+Status.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/InteractionStatusUpdate.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/InteractionsInput.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/MCPServer.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/MCPServerToolCallDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/MCPServerToolCallStep.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/MCPServerToolResultDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/MCPServerToolResultStep.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/MediaProcessing.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/MediaResolution.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ModalityTokens.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/Model.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ModelOutputStep.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/PlaceCitation.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ProcessingCallDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ProcessingCallStep.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ProcessingResultDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ProcessingResultStep.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ResponseFormat.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ResponseModality.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ReviewSnippet.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/SafetySetting+Method.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/SafetySetting+Threshold.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/SafetySetting.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ServiceTier.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/SmartTranscriptionMode.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/Source+Type.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/Source.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/SpeakerConfig.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/SpeechConfig.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/StaticMediaProcessing.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/Step.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/StepDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/StepDeltaData.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/StepDeltaMetadata.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/StepStart.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/StepStop.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/TextAnnotationDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/TextContent.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/TextDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/TextResponseFormat+MimeType.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/TextResponseFormat.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ThinkingLevel.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ThinkingSummaries.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ThoughtSignatureDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ThoughtStep.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ThoughtSummaryContent.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ThoughtSummaryDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/Tool.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/ToolChoiceConfig.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/TranscriptionConfig.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/TranscriptionMode.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/URLCitation.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/URLContext.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/URLContextCallArguments.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/URLContextCallDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/URLContextCallStep.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/URLContextResult+Status.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/URLContextResult.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/URLContextResultDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/URLContextResultStep.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/Usage.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/UserInputStep.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/VerbatimTranscriptionMode.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/VideoConfig+Task.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/VideoConfig.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/VideoContent+MimeType.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/VideoContent.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/VideoDelta+MimeType.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/VideoDelta.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/VideoResponseFormat+AspectRatio.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/VideoResponseFormat+Delivery.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/VideoResponseFormat+Resolution.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/VideoResponseFormat.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/WebhookConfig.swift create mode 100644 GeminiLanguageModel/Sources/InteractionsDataModels/WordInfo.swift create mode 100644 GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiInteractionsRequestTranslatorTests.swift create mode 100644 GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiInteractionsTranscriptTranslatorTests.swift create mode 100644 GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiLanguageModelInteractionsTests.swift diff --git a/GeminiLanguageModel/Package.swift b/GeminiLanguageModel/Package.swift index a0dd3e7a8ec..5bd2f441d40 100644 --- a/GeminiLanguageModel/Package.swift +++ b/GeminiLanguageModel/Package.swift @@ -37,6 +37,8 @@ let package = Package( dependencies: [ "GeminiAPIClient", "GeminiAPIDataModels", + "InteractionsDataModels", + "GeminiSharedDataModels", ], swiftSettings: defaultSwiftSettings ), @@ -66,7 +68,9 @@ let package = Package( .target( name: "GeminiAPIClient", dependencies: [ - "GeminiAPIDataModels" + "GeminiAPIDataModels", + "InteractionsDataModels", + "GeminiSharedDataModels", ], swiftSettings: defaultSwiftSettings ), @@ -78,8 +82,28 @@ let package = Package( ], swiftSettings: defaultSwiftSettings ), + .target( + name: "GeminiSharedDataModels", + swiftSettings: [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("MemberImportVisibility"), + ] + ), .target( name: "GeminiAPIDataModels", + dependencies: [ + "GeminiSharedDataModels" + ], + swiftSettings: [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("MemberImportVisibility"), + ] + ), + .target( + name: "InteractionsDataModels", + dependencies: [ + "GeminiSharedDataModels" + ], swiftSettings: [ .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("MemberImportVisibility"), diff --git a/GeminiLanguageModel/Sources/GeminiAPIClient/GeminiAPIClient.swift b/GeminiLanguageModel/Sources/GeminiAPIClient/GeminiAPIClient.swift index 8d0f4ed7a23..ff742ae9e19 100644 --- a/GeminiLanguageModel/Sources/GeminiAPIClient/GeminiAPIClient.swift +++ b/GeminiLanguageModel/Sources/GeminiAPIClient/GeminiAPIClient.swift @@ -13,6 +13,8 @@ // limitations under the License. package import GeminiAPIDataModels +package import GeminiSharedDataModels +package import InteractionsDataModels #if canImport(Darwin) package import Foundation @@ -107,6 +109,27 @@ package struct GeminiAPIClient: Sendable { return try JSONDecoder().decode(CountTokensResponse.self, from: bodyData) } + /// Sends an interaction generation request and delivers responses asynchronously as a + /// backpressured `InteractionStream` sequence. + /// + /// - Parameter request: The structured interaction generation request. + /// - Returns: A backpressured `InteractionStream` async sequence. + /// - Throws: `GeminiAPIError.apiError` on API failures, or standard network errors. + package func interactionStream( + for request: CreateModelInteraction + ) async throws -> InteractionStream { + let urlRequest = try await makeInteractionsURLRequest(body: request) + + let (lines, response) = try await httpClient.lines(for: urlRequest) + + if response.statusCode != 200 { + let bodyData = try await collectBody(from: lines) + throw parseError(from: bodyData, statusCode: response.statusCode, response: response) + } + + return InteractionStream(lines: lines, response: response) + } + /// Assembles the complete request URL for the configured model resource and endpoint. /// /// - Parameters: @@ -159,6 +182,72 @@ package struct GeminiAPIClient: Sendable { return urlRequest } + /// Assembles the complete request URL for the Interactions API based on the configured model + /// resource and endpoint. + /// + /// The path is constructed by extracting any parent project or location hierarchy preceding + /// `/models/` or `/publishers/` in `modelResource.urlResourceName`, routing to `/interactions`. + /// + /// - Parameter queryItems: An optional array of query items to append to the URL. + /// - Returns: The resolved Interactions API `URL`. + /// - Throws: `URLError(.badURL)` if the components do not form a valid URL. + func makeInteractionsRequestURL( + queryItems: [URLQueryItem]? = nil + ) throws -> URL { + var components = URLComponents() + components.scheme = endpointConfiguration.scheme + components.host = endpointConfiguration.host + components.port = endpointConfiguration.port + + let slashSet = CharacterSet(charactersIn: "/") + let sanitizedVersion = endpointConfiguration.apiVersion.trimmingCharacters(in: slashSet) + let versionComponent = sanitizedVersion.isEmpty ? "" : "/\(sanitizedVersion)" + let sanitizedResourcePath = modelResource.urlResourceName.trimmingCharacters(in: slashSet) + + let parent: String + if sanitizedResourcePath.hasPrefix("models/") { + parent = "" + } else if let range = sanitizedResourcePath.range(of: "/publishers/") { + parent = String(sanitizedResourcePath[..( + queryItems: [URLQueryItem]? = nil, + body: Body + ) async throws -> URLRequest { + let requestURL = try makeInteractionsRequestURL(queryItems: queryItems) + + var urlRequest = URLRequest(url: requestURL) + urlRequest.httpMethod = "POST" + urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + urlRequest.setValue("text/event-stream", forHTTPHeaderField: "Accept") + + if let headerProvider { + for (key, value) in try await headerProvider() { + urlRequest.setValue(value, forHTTPHeaderField: key) + } + } + + urlRequest.httpBody = try JSONEncoder().encode(body) + return urlRequest + } + private func collectBody(from lines: HTTPAsyncLineSequence) async throws -> Data { return try await lines.reduce(into: Data()) { result, line in if !result.isEmpty { @@ -294,6 +383,180 @@ package struct GenerateContentStream: AsyncSequence, Sendable { } } +// MARK: - Interaction Stream + +/// An asynchronous sequence of `InteractionSSEEvent` chunks streamed from Gemini Interactions API. +/// +/// Iterates on-demand over Server-Sent Events with backpressure and zero unstructured `Task` +/// allocation. Cancellation propagates directly to the underlying network stream. +@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +package struct InteractionStream: AsyncSequence, Sendable { + /// The type of element produced by this asynchronous sequence. + package typealias Element = InteractionSSEEvent + + private let lines: HTTPAsyncLineSequence + private let response: HTTPURLResponse + + /// Initializes a new interaction stream from an underlying HTTP line sequence and response metadata. + /// + /// - Parameters: + /// - lines: The sequence of text lines received from the server. + /// - response: The initial HTTP response headers and status code. + init(lines: HTTPAsyncLineSequence, response: HTTPURLResponse) { + self.lines = lines + self.response = response + } + + /// Creates an asynchronous iterator over the stream of interaction SSE events. + /// + /// - Returns: An `AsyncIterator` instance. + package func makeAsyncIterator() -> AsyncIterator { + AsyncIterator(linesIterator: lines.makeAsyncIterator(), response: response) + } + + /// An asynchronous iterator over Server-Sent Events decoded into + /// `InteractionSSEEvent` chunks. + package struct AsyncIterator: AsyncIteratorProtocol { + private var linesIterator: HTTPAsyncLineSequence.AsyncIterator + private let response: HTTPURLResponse + private let decoder = JSONDecoder() + private var sseDataBuffer = "" + private var extraLinesBuffer = "" + private var currentEvent: String? + + init(linesIterator: HTTPAsyncLineSequence.AsyncIterator, response: HTTPURLResponse) { + self.linesIterator = linesIterator + self.response = response + } + + /// Asynchronously advances to and returns the next `InteractionSSEEvent` chunk. + /// + /// - Returns: The next decoded `InteractionSSEEvent`, or `nil` if the stream has finished. + /// - Throws: An error if reading or decoding fails, or if a mid-stream API error occurs. + package mutating func next() async throws -> InteractionSSEEvent? { + while let line = try await linesIterator.next() { + // Empty line marks the end of an SSE event + if line.isEmpty || line.allSatisfy({ $0.isWhitespace }) { + if !sseDataBuffer.isEmpty { + let dataString = sseDataBuffer + let eventName = currentEvent + sseDataBuffer = "" + currentEvent = nil + if dataString == "[DONE]" { + return nil + } + return try decodeEventData(dataString, eventName: eventName) + } + currentEvent = nil + continue + } + + // SSE comment line (e.g. ": keep-alive") + if line.hasPrefix(":") { + continue + } + + // SSE event field + if line.hasPrefix("event:") { + var eventContent = line.dropFirst(6) + if eventContent.hasPrefix(" ") { + eventContent = eventContent.dropFirst() + } + currentEvent = String(eventContent) + continue + } + + // SSE control fields (e.g. "id: 1", "retry: 5000") + if line.hasPrefix("id:") || line.hasPrefix("retry:") { + continue + } + + // SSE data field + if line.hasPrefix("data:") { + var dataContent = line.dropFirst(5) + // The SSE specification only allows a single leading space after the colon. + if dataContent.hasPrefix(" ") { + dataContent = dataContent.dropFirst() + } + if dataContent == "[DONE]" { + sseDataBuffer = "[DONE]" + continue + } + if !dataContent.isEmpty { + if !sseDataBuffer.isEmpty { + sseDataBuffer.append("\n") + } + sseDataBuffer.append(contentsOf: dataContent) + } + continue + } + + // Non-SSE payload line (e.g. raw JSON error block or unexpected content) + extraLinesBuffer.append(line) + extraLinesBuffer.append("\n") + } + + // Flush any pending SSE event data + if !sseDataBuffer.isEmpty { + let dataString = sseDataBuffer + let eventName = currentEvent + sseDataBuffer = "" + currentEvent = nil + if dataString == "[DONE]" { + return nil + } + return try decodeEventData(dataString, eventName: eventName) + } + + // If extra non-SSE lines were accumulated, parse as error + let trimmedExtra = extraLinesBuffer.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmedExtra.isEmpty { + let data = Data(trimmedExtra.utf8) + throw parseError(from: data, statusCode: response.statusCode, response: response) + } + + return nil + } + + private func decodeEventData( + _ jsonString: String, + eventName: String? = nil + ) throws -> InteractionSSEEvent { + let data = Data(jsonString.utf8) + // Fast-path: only attempt GoogleCloudAPIError decoding if payload contains an "error" key. + if jsonString.contains("\"error\""), + let apiError = try? decoder.decode(GoogleCloudAPIError.self, from: data) + { + let headerRetryAfter = parseRetryAfterHeader(from: response) + let resolvedError = headerRetryAfter.map { apiError.withRetryDelay($0) } ?? apiError + throw GeminiAPIError.apiError(resolvedError) + } + if let event = try? decoder.decode(InteractionSSEEvent.self, from: data) { + return event + } + if let streamEvent = try? decoder.decode(InteractionSSEStreamEvent.self, from: data), + let event = streamEvent.data + { + return event + } + // If neither worked and we have an SSE eventName, attempt injecting it into the JSON object + if let eventName, + var jsonObject = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] + { + if jsonObject["type"] == nil && jsonObject["event_type"] == nil { + jsonObject["event_type"] = eventName + if let modifiedData = try? JSONSerialization.data(withJSONObject: jsonObject) { + if let event = try? decoder.decode(InteractionSSEEvent.self, from: modifiedData) { + return event + } + } + } + } + return try decoder.decode(InteractionSSEEvent.self, from: data) + } + } +} + // MARK: - Internal Error Parsing Helpers @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) diff --git a/GeminiLanguageModel/Sources/GeminiAPIClient/GeminiAPIError.swift b/GeminiLanguageModel/Sources/GeminiAPIClient/GeminiAPIError.swift index ed36abb8440..7d2affb0ad1 100644 --- a/GeminiLanguageModel/Sources/GeminiAPIClient/GeminiAPIError.swift +++ b/GeminiLanguageModel/Sources/GeminiAPIClient/GeminiAPIError.swift @@ -14,6 +14,7 @@ import Foundation package import GeminiAPIDataModels +package import GeminiSharedDataModels #if canImport(FoundationNetworking) import FoundationNetworking diff --git a/GeminiLanguageModel/Sources/GeminiAPIClient/HTTPLineDecoder.swift b/GeminiLanguageModel/Sources/GeminiAPIClient/HTTPLineDecoder.swift index e930925fd11..c277981b560 100644 --- a/GeminiLanguageModel/Sources/GeminiAPIClient/HTTPLineDecoder.swift +++ b/GeminiLanguageModel/Sources/GeminiAPIClient/HTTPLineDecoder.swift @@ -14,6 +14,9 @@ package import Foundation +private let lineFeed: UInt8 = 0x0A +private let carriageReturn: UInt8 = 0x0D + /// An incremental decoder that extracts UTF-8 lines of text from streaming byte chunks. package struct HTTPLineDecoder: Sendable { private var buffer = Data() @@ -34,7 +37,7 @@ package struct HTTPLineDecoder: Sendable { if pendingCR { pendingCR = false - if data[searchStartIndex] == 0x0A /* LF (\n) */ { + if data[searchStartIndex] == lineFeed { searchStartIndex = data.index(after: searchStartIndex) } } @@ -45,7 +48,7 @@ package struct HTTPLineDecoder: Sendable { // of the chunk if one delimiter type (e.g. CR) is missing from the payload. guard let nextNewlineIndex = data[searchStartIndex...].firstIndex(where: { - $0 == 0x0A /* LF (\n) */ || $0 == 0x0D /* CR (\r) */ + $0 == lineFeed || $0 == carriageReturn }) else { buffer.append(data[searchStartIndex...]) @@ -57,7 +60,7 @@ package struct HTTPLineDecoder: Sendable { if pendingCR { pendingCR = false - if byte == 0x0A /* LF (\n) */ && nextNewlineIndex == searchStartIndex { + if byte == lineFeed && nextNewlineIndex == searchStartIndex { searchStartIndex = data.index(after: nextNewlineIndex) continue } @@ -77,7 +80,7 @@ package struct HTTPLineDecoder: Sendable { } lines.append(line) - if byte == 0x0D /* CR (\r) */ { + if byte == carriageReturn { pendingCR = true } else { pendingCR = false diff --git a/GeminiLanguageModel/Sources/GeminiAPIClient/ModelResource.swift b/GeminiLanguageModel/Sources/GeminiAPIClient/ModelResource.swift index 8954bf0862d..5e40d3a6b0c 100644 --- a/GeminiLanguageModel/Sources/GeminiAPIClient/ModelResource.swift +++ b/GeminiLanguageModel/Sources/GeminiAPIClient/ModelResource.swift @@ -21,7 +21,7 @@ package struct ModelResource: Sendable, Hashable, Equatable { /// /// - Note: While the format is consistent (e.g., `gemini-3.8-flash`), the specific models /// available and their exact identifiers may differ between backend environments. - let modelID: String + package let modelID: String /// The fully qualified resource name used when constructing HTTP request URLs. /// @@ -35,7 +35,7 @@ package struct ModelResource: Sendable, Hashable, Equatable { /// ### Gemini Enterprise Agent Platform /// The format is identical for both direct and Firebase AI Logic requests: /// - `projects/{projectID}/locations/{locationID}/publishers/google/models/{modelID}` - let urlResourceName: String + package let urlResourceName: String /// The canonical resource name used when serializing nested JSON payloads. /// @@ -46,7 +46,7 @@ package struct ModelResource: Sendable, Hashable, Equatable { /// ### Formats /// - **Gemini Developer API:** `models/{modelID}` /// - **Gemini Enterprise Agent Platform:** `publishers/google/models/{modelID}` - let payloadResourceName: String + package let payloadResourceName: String /// Creates a new model resource configuration. /// diff --git a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/CountTokensRequest.swift b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/CountTokensRequest.swift index 83e8c69d451..0fcafef23dd 100644 --- a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/CountTokensRequest.swift +++ b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/CountTokensRequest.swift @@ -13,6 +13,7 @@ // limitations under the License. import Foundation +package import GeminiSharedDataModels /// An internal data model for `CountTokensRequest`. /// diff --git a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/FunctionCall.swift b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/FunctionCall.swift index ce8a967a3ef..564734c8b80 100644 --- a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/FunctionCall.swift +++ b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/FunctionCall.swift @@ -13,6 +13,7 @@ // limitations under the License. import Foundation +package import GeminiSharedDataModels /// An internal data model for `FunctionCall`. /// diff --git a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/FunctionDeclaration.swift b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/FunctionDeclaration.swift index 328b614ff82..4dce4a00a9b 100644 --- a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/FunctionDeclaration.swift +++ b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/FunctionDeclaration.swift @@ -13,6 +13,7 @@ // limitations under the License. import Foundation +package import GeminiSharedDataModels /// An internal data model for `FunctionDeclaration`. /// diff --git a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/FunctionResponse.swift b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/FunctionResponse.swift index a90265510fc..8fd513d4ce7 100644 --- a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/FunctionResponse.swift +++ b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/FunctionResponse.swift @@ -13,6 +13,7 @@ // limitations under the License. import Foundation +package import GeminiSharedDataModels /// An internal data model for `FunctionResponse`. /// diff --git a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/GenerationConfig.swift b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/GenerationConfig.swift index 34b92c05f02..8500b9f4778 100644 --- a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/GenerationConfig.swift +++ b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/GenerationConfig.swift @@ -13,6 +13,7 @@ // limitations under the License. import Foundation +package import GeminiSharedDataModels /// An internal data model for `GenerationConfig`. /// diff --git a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/Part.swift b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/Part.swift index 5617132e2f0..a7676b51d92 100644 --- a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/Part.swift +++ b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/Part.swift @@ -13,6 +13,7 @@ // limitations under the License. import Foundation +package import GeminiSharedDataModels /// An internal data model for `Part`. /// diff --git a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/Schema.swift b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/Schema.swift index a5b6c4aaec6..e7f25071e7c 100644 --- a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/Schema.swift +++ b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/Schema.swift @@ -13,6 +13,7 @@ // limitations under the License. import Foundation +package import GeminiSharedDataModels /// An internal data model for `Schema`. /// diff --git a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/TemplateFunction.swift b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/TemplateFunction.swift index d4b9c248421..73120a55a6e 100644 --- a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/TemplateFunction.swift +++ b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/TemplateFunction.swift @@ -13,6 +13,7 @@ // limitations under the License. import Foundation +package import GeminiSharedDataModels /// An internal data model for `TemplateFunction`. /// diff --git a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/TemplateGenerateContentRequest.swift b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/TemplateGenerateContentRequest.swift index 939e54f5127..cb21fdf17a6 100644 --- a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/TemplateGenerateContentRequest.swift +++ b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/TemplateGenerateContentRequest.swift @@ -13,6 +13,7 @@ // limitations under the License. import Foundation +package import GeminiSharedDataModels /// An internal data model for `TemplateGenerateContentRequest`. /// diff --git a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/TextResponseFormat.swift b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/TextResponseFormat.swift index 46a893a6b2a..8047239f2bc 100644 --- a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/TextResponseFormat.swift +++ b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/TextResponseFormat.swift @@ -13,6 +13,7 @@ // limitations under the License. import Foundation +package import GeminiSharedDataModels /// An internal data model for `TextResponseFormat`. /// diff --git a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/ToolCall.swift b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/ToolCall.swift index 11ebc045333..3161578c561 100644 --- a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/ToolCall.swift +++ b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/ToolCall.swift @@ -13,6 +13,7 @@ // limitations under the License. import Foundation +package import GeminiSharedDataModels /// An internal data model for `ToolCall`. /// diff --git a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/ToolResponse.swift b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/ToolResponse.swift index 2732e26e427..e72baa05841 100644 --- a/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/ToolResponse.swift +++ b/GeminiLanguageModel/Sources/GeminiAPIDataModels/GenerateContent/ToolResponse.swift @@ -13,6 +13,7 @@ // limitations under the License. import Foundation +package import GeminiSharedDataModels /// An internal data model for `ToolResponse`. /// diff --git a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiErrorMapper.swift b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiErrorMapper.swift index e28908207ae..77d56ac62f4 100644 --- a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiErrorMapper.swift +++ b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiErrorMapper.swift @@ -17,6 +17,7 @@ import FoundationModels import GeminiAPIClient import GeminiAPIDataModels + import GeminiSharedDataModels /// Maps Gemini API and network errors to `FoundationModels.LanguageModelError` or /// `GeminiLanguageModel.Error`. diff --git a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiInteractionsRequestTranslator.swift b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiInteractionsRequestTranslator.swift new file mode 100644 index 00000000000..6ad3bed6df2 --- /dev/null +++ b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiInteractionsRequestTranslator.swift @@ -0,0 +1,122 @@ +// 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 canImport(FoundationModels) && compiler(>=6.4) + import Foundation + import FoundationModels + import GeminiAPIClient + import GeminiSharedDataModels + import InteractionsDataModels + + /// Translates Foundation Models requests into Gemini Interactions API data models. + @available(iOS 27.0, macOS 27.0, watchOS 27.0, visionOS 27.0, *) + @available(tvOS, unavailable) + enum GeminiInteractionsRequestTranslator { + /// Translates a `LanguageModelExecutorGenerationRequest` into a `CreateModelInteraction`. + /// + /// - Parameters: + /// - request: The generation request from the Foundation Models session. + /// - modelResource: The model resource specifying identifiers. + /// - Returns: A `CreateModelInteraction` configured for the Gemini Interactions API. + /// - Throws: An error if transcript, schema, or tool translation fails. + static func translate( + _ request: LanguageModelExecutorGenerationRequest, + modelResource: ModelResource + ) throws -> CreateModelInteraction { + let (steps, systemInstruction) = try GeminiInteractionsTranscriptTranslator.translate( + request.transcript + ) + let modelIdentifier = + modelResource.payloadResourceName.hasPrefix("publishers/") + ? modelResource.payloadResourceName + : modelResource.modelID + + let tools = try translateTools(request.enabledToolDefinitions) + let responseFormat = try translateResponseFormat(schema: request.schema) + let generationConfig = translateGenerationConfig( + toolCallingMode: request.generationOptions.toolCallingMode + ) + + return CreateModelInteraction( + generationConfig: generationConfig, + input: .stepList(steps), + model: Model(rawValue: modelIdentifier), + responseFormat: responseFormat, + store: false, + stream: true, + systemInstruction: systemInstruction, + tools: tools + ) + } + + /// Translates an optional `GenerationSchema` into a `JSONValue` representing the JSON Schema. + /// + /// - Parameter schema: An optional generation schema specifying structured output constraints. + /// - Returns: A `JSONValue` representation of the schema, or `nil` if `schema` is `nil`. + /// - Throws: An error if encoding the schema fails. + static func translateResponseFormat( + schema: GenerationSchema? + ) throws -> JSONValue? { + guard let schema else { return nil } + let jsonSchema = try schema.toGeminiJSONSchema() + let data = try JSONEncoder().encode(jsonSchema) + return try JSONDecoder().decode(JSONValue.self, from: data) + } + + /// Translates enabled tool definitions into a list of Gemini Interactions `Tool` objects. + /// + /// - Parameter enabledToolDefinitions: Tool definitions registered for the request. + /// - Returns: A list of `Tool` objects, or `nil` if empty. + /// - Throws: An error if schema translation fails. + static func translateTools( + _ enabledToolDefinitions: [Transcript.ToolDefinition] + ) throws -> [InteractionsDataModels.Tool]? { + guard !enabledToolDefinitions.isEmpty else { return nil } + + return try enabledToolDefinitions.map { toolDefinition in + let parameters = try toolDefinition.parameters.toGeminiJSONValue() + let function = Function( + description: toolDefinition.description, + name: toolDefinition.name, + parameters: parameters + ) + return .function(function) + } + } + + /// Translates tool calling mode options into an Interactions `GenerationConfig`. + /// + /// - Parameter toolCallingMode: The tool calling mode options from the request. + /// - Returns: A `GenerationConfig` configured with `toolChoice`, or `nil` if unspecified. + static func translateGenerationConfig( + toolCallingMode: GenerationOptions.ToolCallingMode? + ) -> GenerationConfig? { + guard let mode = toolCallingMode else { return nil } + + let choice: String + switch mode.kind { + case .allowed: + choice = "auto" + case .required: + choice = "any" + case .disallowed: + choice = "none" + @unknown default: + choice = "auto" + } + + return GenerationConfig(toolChoice: .string(choice)) + } + } +#endif diff --git a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiInteractionsTranscriptTranslator.swift b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiInteractionsTranscriptTranslator.swift new file mode 100644 index 00000000000..71cae6679a3 --- /dev/null +++ b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiInteractionsTranscriptTranslator.swift @@ -0,0 +1,210 @@ +// 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 canImport(FoundationModels) && compiler(>=6.4) + import Foundation + import FoundationModels + import GeminiSharedDataModels + import InteractionsDataModels + + /// Translates Apple's `FoundationModels.Transcript` into Gemini Interactions API step requests. + @available(iOS 27.0, macOS 27.0, watchOS 27.0, visionOS 27.0, *) + @available(tvOS, unavailable) + enum GeminiInteractionsTranscriptTranslator { + /// Translates a transcript into interaction steps and an optional system instruction. + /// + /// - Parameter transcript: The conversation history transcript. + /// - Returns: A tuple containing the list of interaction steps and an optional system instruction string. + /// - Throws: `LanguageModelError.unsupportedTranscriptContent` if unsupported entries or + /// segments are present. + static func translate( + _ transcript: Transcript + ) throws -> ( + steps: [Step], systemInstruction: String? + ) { + var steps: [Step] = [] + var systemInstructions: [String] = [] + + for entry in transcript { + switch entry { + case .instructions(let instructions): + let text = try extractText(from: instructions.segments, in: entry) + if !text.isEmpty { + systemInstructions.append(text) + } + + case .prompt(let prompt): + let text = try extractText(from: prompt.segments, in: entry) + let userInput = UserInputStep(content: [.textContent(TextContent(text: text))]) + steps.append(.userInputStep(userInput)) + + case .response(let response): + let text = try extractText(from: response.segments, in: entry) + let modelOutput = ModelOutputStep(content: [.textContent(TextContent(text: text))]) + steps.append(.modelOutputStep(modelOutput)) + + case .reasoning(let reasoning): + let text = try extractOptionalText(from: reasoning.segments, in: entry) + let summary = text.map { [ThoughtSummaryContent.textContent(TextContent(text: $0))] } + let thought = ThoughtStep(signature: reasoning.signature, summary: summary) + steps.append(.thoughtStep(thought)) + + case .toolCalls(let toolCalls): + for call in toolCalls { + let args: [String: JSONValue]? + if case .structure(let properties, _) = call.arguments.kind { + args = properties.isEmpty ? nil : properties.mapValues { jsonValue(from: $0) } + } else { + args = nil + } + let functionCall = FunctionCallStep( + arguments: args, + id: call.id, + name: call.toolName + ) + steps.append(.functionCallStep(functionCall)) + } + + case .toolOutput(let toolOutput): + if toolOutput.segments.isEmpty { + let resultStep = FunctionResultStep( + callId: toolOutput.id, + name: toolOutput.toolName, + result: .object(["result": .null]) + ) + steps.append(.functionResultStep(resultStep)) + } else { + for segment in toolOutput.segments { + let response = try extractResponse(from: segment, in: entry) + let resultStep = FunctionResultStep( + callId: toolOutput.id, + name: toolOutput.toolName, + result: .object(response) + ) + steps.append(.functionResultStep(resultStep)) + } + } + + @unknown default: + throw makeUnsupportedError( + entry, + description: "Unsupported transcript entry." + ) + } + } + + let systemInstruction = + systemInstructions.isEmpty ? nil : systemInstructions.joined(separator: "\n") + return (steps: steps, systemInstruction: systemInstruction) + } + + // MARK: - Private Helpers + + private static func makeUnsupportedError( + _ entry: Transcript.Entry, + description: String + ) -> LanguageModelError { + LanguageModelError.unsupportedTranscriptContent( + LanguageModelError.UnsupportedTranscriptContent( + unsupportedContent: [entry], + debugDescription: description + ) + ) + } + + private static func jsonValue(from content: GeneratedContent) -> JSONValue { + switch content.kind { + case .null: + return .null + case .bool(let value): + return .bool(value) + case .number(let value): + return .number(value) + case .string(let value): + return .string(value) + case .array(let values): + return .array(values.map { jsonValue(from: $0) }) + case .structure(let properties, _): + return .object(properties.mapValues { jsonValue(from: $0) }) + @unknown default: + return .null + } + } + + private static func extractResponse( + from segment: Transcript.Segment, + in entry: Transcript.Entry + ) throws -> [String: JSONValue] { + switch segment { + case .structure(let structuredSegment): + let val = jsonValue(from: structuredSegment.content) + if case .object(let obj) = val { + return obj + } else { + return ["result": val] + } + case .text(let textSegment): + return ["result": .string(textSegment.content)] + + case .attachment: + throw makeUnsupportedError( + entry, + description: "Attachment segments in tool output are not supported." + ) + @unknown default: + throw makeUnsupportedError( + entry, + description: "Unsupported segment in tool output." + ) + } + } + + private static func extractText( + from segments: [Transcript.Segment], + in entry: Transcript.Entry + ) throws -> String { + var text = "" + for segment in segments { + switch segment { + case .text(let textSegment): + text.append(textSegment.content) + case .attachment: + throw makeUnsupportedError( + entry, + description: "Attachment segments in transcript are not supported." + ) + case .structure: + throw makeUnsupportedError( + entry, + description: "Structured segments in transcript are not supported." + ) + @unknown default: + throw makeUnsupportedError( + entry, + description: "Unsupported transcript segment." + ) + } + } + return text + } + + private static func extractOptionalText( + from segments: [Transcript.Segment], + in entry: Transcript.Entry + ) throws -> String? { + let text = try extractText(from: segments, in: entry) + return text.isEmpty ? nil : text + } + } +#endif diff --git a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel+Error.swift b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel+Error.swift index 4ce768a5d6e..b312275f168 100644 --- a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel+Error.swift +++ b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel+Error.swift @@ -39,26 +39,12 @@ public struct ServiceUnavailable: Sendable, CustomDebugStringConvertible { /// A debug description of the service unavailability. public let debugDescription: String - - /// Creates a service unavailable error instance. - /// - /// - Parameter debugDescription: A debug description of the failure. - init(debugDescription: String) { - self.debugDescription = debugDescription - } } /// Information about a network failure when communicating with the Gemini service. public struct NetworkFailure: Sendable, CustomDebugStringConvertible { /// A debug description of the network failure. public let debugDescription: String - - /// Creates a network failure error instance. - /// - /// - Parameter debugDescription: A debug description of the failure. - init(debugDescription: String) { - self.debugDescription = debugDescription - } } /// Information about a requested model resource that could not be found. diff --git a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel.swift b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel.swift index a3d8cab83c4..4037beee7c1 100644 --- a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel.swift +++ b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel.swift @@ -42,6 +42,23 @@ @available(iOS 27.0, macOS 27.0, watchOS 27.0, visionOS 27.0, *) @available(tvOS, unavailable) public struct GeminiLanguageModel: Sendable { + /// The API variant used to communicate with Gemini. + public enum APIVariant: Sendable, Hashable, CaseIterable, CustomStringConvertible { + /// The standard content generation API (`:streamGenerateContent`). + case generateContent + /// The Interactions API (`/interactions`). + case interactions + + public var description: String { + switch self { + case .generateContent: + return "Generate Content" + case .interactions: + return "Interactions" + } + } + } + /// The configuration for the executor responsible for translating Foundation Models requests to /// Gemini API calls. public let executorConfiguration: Executor.Configuration @@ -53,17 +70,20 @@ /// - endpointConfiguration: The network endpoint configuration. /// - headerProvider: An optional async provider for dynamic headers (such as auth tokens). /// - configuration: The `URLSessionConfiguration` to use. Defaults to `.ephemeral`. + /// - apiVariant: The API variant to use. Defaults to `.generateContent`. package init( modelResource: ModelResource, endpointConfiguration: EndpointConfiguration, headerProvider: (@Sendable () async throws -> [String: String])? = nil, - configuration: URLSessionConfiguration = .ephemeral + configuration: URLSessionConfiguration = .ephemeral, + apiVariant: APIVariant = .generateContent ) { executorConfiguration = Executor.Configuration( modelResource: modelResource, endpointConfiguration: endpointConfiguration, headerProvider: headerProvider.map { HeaderProvider($0) }, - sessionConfiguration: configuration + sessionConfiguration: configuration, + apiVariant: apiVariant ) } } diff --git a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModelExecutor.swift b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModelExecutor.swift index 055b91b5f7e..808681800ea 100644 --- a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModelExecutor.swift +++ b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModelExecutor.swift @@ -17,6 +17,8 @@ public import FoundationModels package import GeminiAPIClient import GeminiAPIDataModels + import GeminiSharedDataModels + import InteractionsDataModels @available(iOS 27.0, macOS 27.0, watchOS 27.0, visionOS 27.0, *) @available(tvOS, unavailable) @@ -37,6 +39,9 @@ /// The `URLSessionConfiguration` to use. let sessionConfiguration: URLSessionConfiguration + /// The API variant to use for generation. + let apiVariant: APIVariant + /// Initializes an executor configuration. /// /// - Parameters: @@ -44,16 +49,19 @@ /// - endpointConfiguration: The network endpoint configuration. /// - headerProvider: An optional async provider for dynamic headers. /// - sessionConfiguration: The `URLSessionConfiguration` to use. + /// - apiVariant: The API variant to use for generation. Defaults to `.generateContent`. init( modelResource: ModelResource, endpointConfiguration: EndpointConfiguration, headerProvider: HeaderProvider?, - sessionConfiguration: URLSessionConfiguration + sessionConfiguration: URLSessionConfiguration, + apiVariant: APIVariant = .generateContent ) { self.modelResource = modelResource self.endpointConfiguration = endpointConfiguration self.headerProvider = headerProvider self.sessionConfiguration = sessionConfiguration + self.apiVariant = apiVariant } } @@ -78,6 +86,41 @@ to request: LanguageModelExecutorGenerationRequest, model: GeminiLanguageModel, streamingInto channel: LanguageModelExecutorGenerationChannel + ) async throws { + let responseEntryID = UUID().uuidString + let reasoningEntryID = UUID().uuidString + let toolCallsEntryID = UUID().uuidString + let jsonEncoder = JSONEncoder() + + switch configuration.apiVariant { + case .generateContent: + try await respondWithGenerateContent( + to: request, + streamingInto: channel, + responseEntryID: responseEntryID, + reasoningEntryID: reasoningEntryID, + toolCallsEntryID: toolCallsEntryID, + jsonEncoder: jsonEncoder + ) + case .interactions: + try await respondWithInteractions( + to: request, + streamingInto: channel, + responseEntryID: responseEntryID, + reasoningEntryID: reasoningEntryID, + toolCallsEntryID: toolCallsEntryID, + jsonEncoder: jsonEncoder + ) + } + } + + private func respondWithGenerateContent( + to request: LanguageModelExecutorGenerationRequest, + streamingInto channel: LanguageModelExecutorGenerationChannel, + responseEntryID: String, + reasoningEntryID: String, + toolCallsEntryID: String, + jsonEncoder: JSONEncoder ) async throws { let generateRequest = try GeminiRequestTranslator.translate(request) @@ -88,11 +131,6 @@ sessionConfiguration: configuration.sessionConfiguration ) - let responseEntryID = UUID().uuidString - let reasoningEntryID = UUID().uuidString - let toolCallsEntryID = UUID().uuidString - let jsonEncoder = JSONEncoder() - do { let stream = try await client.generateContentStream(for: generateRequest) @@ -179,6 +217,184 @@ throw GeminiErrorMapper.map(error) } } + + private func respondWithInteractions( + to request: LanguageModelExecutorGenerationRequest, + streamingInto channel: LanguageModelExecutorGenerationChannel, + responseEntryID: String, + reasoningEntryID: String, + toolCallsEntryID: String, + jsonEncoder: JSONEncoder + ) async throws { + let interactionRequest = try GeminiInteractionsRequestTranslator.translate( + request, + modelResource: configuration.modelResource + ) + + let client = GeminiAPIClient( + modelResource: configuration.modelResource, + endpointConfiguration: configuration.endpointConfiguration, + headerProvider: configuration.headerProvider, + sessionConfiguration: configuration.sessionConfiguration + ) + + var activeToolCalls: [Int: (id: String, name: String)] = [:] + var lastToolCall: (id: String, name: String)? + var toolCallArgumentsSent: Set = [] + + do { + let stream = try await client.interactionStream(for: interactionRequest) + + for try await event in stream { + try Task.checkCancellation() + + switch event { + case .stepStart(let stepStart): + let index = stepStart.index ?? 0 + if let step = stepStart.step, case .functionCallStep(let fc) = step { + let toolCall = (id: fc.id ?? UUID().uuidString, name: fc.name ?? "") + activeToolCalls[index] = toolCall + lastToolCall = toolCall + if let args = fc.arguments { + let argsString: String + if !args.isEmpty { + let data = try jsonEncoder.encode(JSONValue.object(args)) + argsString = String(decoding: data, as: UTF8.self) + } else { + argsString = "{}" + } + toolCallArgumentsSent.insert(index) + await channel.send( + .toolCalls( + entryID: toolCallsEntryID, + action: .toolCall( + id: toolCall.id, + name: toolCall.name, + action: .appendArguments(argsString, tokenCount: 1) + ) + ) + ) + } + } + + case .stepDelta(let stepDelta): + let index = stepDelta.index ?? 0 + guard let delta = stepDelta.delta else { break } + + switch delta { + case .textDelta(let textDelta): + if let text = textDelta.text, !text.isEmpty { + await channel.send( + .response( + entryID: responseEntryID, + action: .appendText(text, tokenCount: 1) + ) + ) + } + + case .thoughtSummaryDelta(let thoughtDelta): + if case .textContent(let textContent) = thoughtDelta.content, + let text = textContent.text, !text.isEmpty + { + await channel.send( + .reasoning( + entryID: reasoningEntryID, + action: .appendText(text, tokenCount: 1) + ) + ) + } + + case .thoughtSignatureDelta(let signatureDelta): + if let signature = signatureDelta.signature, !signature.isEmpty { + await channel.send( + .reasoning( + entryID: reasoningEntryID, + action: .updateSignature(signature, tokenCount: 0) + ) + ) + } + + case .argumentsDelta(let argsDelta): + let toolCall = + activeToolCalls[index] ?? lastToolCall ?? (id: UUID().uuidString, name: "") + if let args = argsDelta.arguments, !args.isEmpty { + toolCallArgumentsSent.insert(index) + await channel.send( + .toolCalls( + entryID: toolCallsEntryID, + action: .toolCall( + id: toolCall.id, + name: toolCall.name, + action: .appendArguments(args, tokenCount: 1) + ) + ) + ) + } + + default: + break + } + + case .stepStop(let stepStop): + let index = stepStop.index ?? 0 + if let toolCall = activeToolCalls[index] { + if !toolCallArgumentsSent.contains(index) { + await channel.send( + .toolCalls( + entryID: toolCallsEntryID, + action: .toolCall( + id: toolCall.id, + name: toolCall.name, + action: .appendArguments("{}", tokenCount: 1) + ) + ) + ) + } + } + if let index = stepStop.index { + activeToolCalls.removeValue(forKey: index) + toolCallArgumentsSent.remove(index) + } + + case .interactionCompletedEvent(let completed): + if let usage = completed.interaction?.usage { + await channel.send( + .response( + entryID: responseEntryID, + action: .updateUsage( + input: .init( + totalTokenCount: usage.totalInputTokens ?? 0, + cachedTokenCount: usage.totalCachedTokens ?? 0 + ), + output: .init( + totalTokenCount: usage.totalOutputTokens ?? 0, + reasoningTokenCount: usage.totalThoughtTokens ?? 0 + ) + ) + ) + ) + } + + case .errorEvent(let errorEvent): + let message = errorEvent.error?.message ?? "An error occurred during interaction." + let code = errorEvent.error?.code ?? "ERROR" + throw GeminiLanguageModel.Error.apiError( + GeminiLanguageModel.Error.APIError( + code: code, + statusCode: 400, + message: message, + metadata: [:] + ) + ) + + default: + break + } + } + } catch { + throw GeminiErrorMapper.map(error) + } + } } } #endif // canImport(FoundationModels) && compiler(>=6.4) diff --git a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiRequestTranslator.swift b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiRequestTranslator.swift index 2b07dfd151c..29a5826e69c 100644 --- a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiRequestTranslator.swift +++ b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiRequestTranslator.swift @@ -16,6 +16,7 @@ import Foundation import FoundationModels import GeminiAPIDataModels + import GeminiSharedDataModels /// Translates Foundation Models requests into Gemini API data models. @available(iOS 27.0, macOS 27.0, watchOS 27.0, visionOS 27.0, *) diff --git a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiTranscriptTranslator.swift b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiTranscriptTranslator.swift index 31ec06aa57d..1dbe6d7a2b8 100644 --- a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiTranscriptTranslator.swift +++ b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiTranscriptTranslator.swift @@ -16,6 +16,7 @@ import Foundation import FoundationModels import GeminiAPIDataModels + import GeminiSharedDataModels /// Translates Apple's `FoundationModels.Transcript` into Gemini API content requests. @available(iOS 27.0, macOS 27.0, watchOS 27.0, visionOS 27.0, *) diff --git a/GeminiLanguageModel/Sources/GeminiLanguageModel/GenerationSchema+Gemini.swift b/GeminiLanguageModel/Sources/GeminiLanguageModel/GenerationSchema+Gemini.swift index 9dd74a5c1a9..cdc137f319b 100644 --- a/GeminiLanguageModel/Sources/GeminiLanguageModel/GenerationSchema+Gemini.swift +++ b/GeminiLanguageModel/Sources/GeminiLanguageModel/GenerationSchema+Gemini.swift @@ -16,6 +16,7 @@ import Foundation import FoundationModels import GeminiAPIDataModels + import GeminiSharedDataModels import Synchronization @available(iOS 27.0, macOS 27.0, watchOS 27.0, visionOS 27.0, *) diff --git a/GeminiLanguageModel/Sources/GeminiAPIDataModels/Shared/GoogleCloudAPIError.swift b/GeminiLanguageModel/Sources/GeminiSharedDataModels/GoogleCloudAPIError.swift similarity index 100% rename from GeminiLanguageModel/Sources/GeminiAPIDataModels/Shared/GoogleCloudAPIError.swift rename to GeminiLanguageModel/Sources/GeminiSharedDataModels/GoogleCloudAPIError.swift diff --git a/GeminiLanguageModel/Sources/GeminiAPIDataModels/Shared/JSONValue.swift b/GeminiLanguageModel/Sources/GeminiSharedDataModels/JSONValue.swift similarity index 100% rename from GeminiLanguageModel/Sources/GeminiAPIDataModels/Shared/JSONValue.swift rename to GeminiLanguageModel/Sources/GeminiSharedDataModels/JSONValue.swift diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/Agent.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/Agent.swift new file mode 100644 index 00000000000..dc56a6c9d6e --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/Agent.swift @@ -0,0 +1,86 @@ +// 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. + +package import GeminiSharedDataModels + +/// An agent definition for the CreateAgent API. +/// This message is the target for annotation-parser-based JSON parsing. +/// New format: +/// { +/// "id": "customer-sentinel", +/// "base_agent": "", +/// "system_instruction": "...", +/// "base_environment": { "type": "remote", "sources": [...] }, +/// "tools": [ {"type": "code_execution"} ] +/// } +package struct Agent: Codable, Sendable, Equatable, Hashable { + + /// Configuration parameters for the agent. + package let agentConfig: JSONValue? + + /// The base agent to extend. + package let baseAgent: String? + + /// The environment configuration for the agent. + package let baseEnvironment: JSONValue? + + /// Agent description for developers to quickly read and understand. + package let description: String? + + /// The unique identifier for the agent. + package let id: String? + + /// System instruction for the agent. + package let systemInstruction: String? + + /// The tools available to the agent. + package let tools: [AgentTool]? + + /// Creates a new `Agent`. + /// + /// - Parameters: + /// - agentConfig: Configuration parameters for the agent. + /// - baseAgent: The base agent to extend. + /// - baseEnvironment: The environment configuration for the agent. + /// - description: Agent description for developers to quickly read and understand. + /// - id: The unique identifier for the agent. + /// - systemInstruction: System instruction for the agent. + /// - tools: The tools available to the agent. + package init( + agentConfig: JSONValue? = nil, + baseAgent: String? = nil, + baseEnvironment: JSONValue? = nil, + description: String? = nil, + id: String? = nil, + systemInstruction: String? = nil, + tools: [AgentTool]? = nil + ) { + self.agentConfig = agentConfig + self.baseAgent = baseAgent + self.baseEnvironment = baseEnvironment + self.description = description + self.id = id + self.systemInstruction = systemInstruction + self.tools = tools + } + enum CodingKeys: String, CodingKey { + case agentConfig = "agent_config" + case baseAgent = "base_agent" + case baseEnvironment = "base_environment" + case description = "description" + case id = "id" + case systemInstruction = "system_instruction" + case tools = "tools" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/AgentOption.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/AgentOption.swift new file mode 100644 index 00000000000..3671ac0e846 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/AgentOption.swift @@ -0,0 +1,58 @@ +// 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. + +/// The agent to interact with. +package enum AgentOption: Codable, Sendable, Equatable, Hashable { + + /// Gemini Deep Research Agent + case deepResearchProPreview122025 + + /// Gemini Deep Research Agent + case deepResearchPreview042026 + + /// Gemini Deep Research Max Agent + case deepResearchMaxPreview042026 + + /// Use the Antigravity managed agent to perform multi-step tasks that require reasoning, file operations, and tool use. + case antigravityPreview052026 + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) +} + +// MARK: - RawRepresentable Conformance + +extension AgentOption: RawRepresentable { + package var rawValue: String { + switch self { + case .deepResearchProPreview122025: "deep-research-pro-preview-12-2025" + case .deepResearchPreview042026: "deep-research-preview-04-2026" + case .deepResearchMaxPreview042026: "deep-research-max-preview-04-2026" + case .antigravityPreview052026: "antigravity-preview-05-2026" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "deep-research-pro-preview-12-2025": self = .deepResearchProPreview122025 + case "deep-research-preview-04-2026": self = .deepResearchPreview042026 + case "deep-research-max-preview-04-2026": self = .deepResearchMaxPreview042026 + case "antigravity-preview-05-2026": self = .antigravityPreview052026 + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/AgentTool.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/AgentTool.swift new file mode 100644 index 00000000000..d47706eb3c6 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/AgentTool.swift @@ -0,0 +1,22 @@ +// 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. + +/// A tool that the agent can use. +package struct AgentTool: Codable, Sendable, Equatable, Hashable { + + /// Creates a new `AgentTool`. + /// + package init() { + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/AllowedTools+Mode.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/AllowedTools+Mode.swift new file mode 100644 index 00000000000..dbc714fc3bf --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/AllowedTools+Mode.swift @@ -0,0 +1,60 @@ +// 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. + +extension AllowedTools { + /// The mode of the tool choice. + package enum Mode: Codable, Sendable, Equatable, Hashable { + + /// Auto tool choice. + case auto + + /// Any tool choice. + case `any` + + /// No tool choice. + case none + + /// Validated tool choice. + case validated + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension AllowedTools.Mode: RawRepresentable { + package var rawValue: String { + switch self { + case .auto: "auto" + case .`any`: "any" + case .none: "none" + case .validated: "validated" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "auto": self = .auto + case "any": self = .`any` + case "none": self = .none + case "validated": self = .validated + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/AllowedTools.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/AllowedTools.swift new file mode 100644 index 00000000000..c28f6106a4d --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/AllowedTools.swift @@ -0,0 +1,40 @@ +// 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. + +/// The configuration for allowed tools. +package struct AllowedTools: Codable, Sendable, Equatable, Hashable { + + /// The mode of the tool choice. + package let mode: Mode? + + /// The names of the allowed tools. + package let tools: [String]? + + /// Creates a new `AllowedTools`. + /// + /// - Parameters: + /// - mode: The mode of the tool choice. + /// - tools: The names of the allowed tools. + package init( + mode: Mode? = nil, + tools: [String]? = nil + ) { + self.mode = mode + self.tools = tools + } + enum CodingKeys: String, CodingKey { + case mode = "mode" + case tools = "tools" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/AllowlistEntry.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/AllowlistEntry.swift new file mode 100644 index 00000000000..2db3f431ec0 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/AllowlistEntry.swift @@ -0,0 +1,42 @@ +// 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. + +package import GeminiSharedDataModels + +/// A single domain allowlist rule with optional header injection. +package struct AllowlistEntry: Codable, Sendable, Equatable, Hashable { + + /// Domain to allow outbound requests to. Supports wildcards (e.g. '*.googleapis.com'). Use '*' to allow all domains. + package let domain: String? + + /// Headers to inject on all outbound requests matching this domain. Accepts a single dict or a list of dicts. The egress proxy injects these automatically. + package let transform: JSONValue? + + /// Creates a new `AllowlistEntry`. + /// + /// - Parameters: + /// - domain: Domain to allow outbound requests to. Supports wildcards (e.g. '*.googleapis.com'). Use '*' to allow all domains. + /// - transform: Headers to inject on all outbound requests matching this domain. Accepts a single dict or a list of dicts. The egress proxy injects these automatically. + package init( + domain: String?, + transform: JSONValue? = nil + ) { + self.domain = domain + self.transform = transform + } + enum CodingKeys: String, CodingKey { + case domain = "domain" + case transform = "transform" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/Annotation.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/Annotation.swift new file mode 100644 index 00000000000..ed8f550c81e --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/Annotation.swift @@ -0,0 +1,22 @@ +// 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. + +/// Citation information for model-generated content. +package struct Annotation: Codable, Sendable, Equatable, Hashable { + + /// Creates a new `Annotation`. + /// + package init() { + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/AntigravityAgentConfig.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/AntigravityAgentConfig.swift new file mode 100644 index 00000000000..b5c7b06cde6 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/AntigravityAgentConfig.swift @@ -0,0 +1,46 @@ +// 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. + +/// Configuration for the Antigravity agent runtime. +/// Provides server-side control over the agent's execution environment +/// and tool configuration. +package struct AntigravityAgentConfig: Codable, Sendable, Equatable, Hashable { + + /// Max total tokens for the agent run. + package let maxTotalTokens: String? + + /// The model to use for agent reasoning. + package let model: String? + + package let type: String? + + /// Creates a new `AntigravityAgentConfig`. + /// + /// - Parameters: + /// - maxTotalTokens: Max total tokens for the agent run. + /// - model: The model to use for agent reasoning. + package init( + maxTotalTokens: String? = nil, + model: String? = nil + ) { + self.maxTotalTokens = maxTotalTokens + self.model = model + self.type = "antigravity" + } + enum CodingKeys: String, CodingKey { + case maxTotalTokens = "max_total_tokens" + case model = "model" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ArgumentsDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ArgumentsDelta.swift new file mode 100644 index 00000000000..13f8a98073a --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ArgumentsDelta.swift @@ -0,0 +1,36 @@ +// 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. + +/// An internal data model for `ArgumentsDelta`. +package struct ArgumentsDelta: Codable, Sendable, Equatable, Hashable { + + package let arguments: String? + + package let type: String? + + /// Creates a new `ArgumentsDelta`. + /// + /// - Parameters: + /// - arguments: For more details, see ``arguments``. + package init( + arguments: String? = nil + ) { + self.arguments = arguments + self.type = "arguments_delta" + } + enum CodingKeys: String, CodingKey { + case arguments = "arguments" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/AudioContent+MimeType.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/AudioContent+MimeType.swift new file mode 100644 index 00000000000..03bd5611817 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/AudioContent+MimeType.swift @@ -0,0 +1,105 @@ +// 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. + +extension AudioContent { + /// The mime type of the audio. + package enum MimeType: Codable, Sendable, Equatable, Hashable { + + /// WAV audio format + case wav + + /// MP3 audio format + case mp3 + + /// AIFF audio format + case aiff + + /// AAC audio format + case aac + + /// OGG audio format + case ogg + + /// FLAC audio format + case flac + + /// MPEG audio format + case mpeg + + /// M4A audio format + case m4a + + /// L16 audio format + case l16 + + /// OPUS audio format + case opus + + /// ALAW audio format + case alaw + + /// MULAW audio format + case mulaw + + /// WEBM audio format + case webm + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension AudioContent.MimeType: RawRepresentable { + package var rawValue: String { + switch self { + case .wav: "audio/wav" + case .mp3: "audio/mp3" + case .aiff: "audio/aiff" + case .aac: "audio/aac" + case .ogg: "audio/ogg" + case .flac: "audio/flac" + case .mpeg: "audio/mpeg" + case .m4a: "audio/m4a" + case .l16: "audio/l16" + case .opus: "audio/opus" + case .alaw: "audio/alaw" + case .mulaw: "audio/mulaw" + case .webm: "audio/webm" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "audio/wav": self = .wav + case "audio/mp3": self = .mp3 + case "audio/aiff": self = .aiff + case "audio/aac": self = .aac + case "audio/ogg": self = .ogg + case "audio/flac": self = .flac + case "audio/mpeg": self = .mpeg + case "audio/m4a": self = .m4a + case "audio/l16": self = .l16 + case "audio/opus": self = .opus + case "audio/alaw": self = .alaw + case "audio/mulaw": self = .mulaw + case "audio/webm": self = .webm + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/AudioContent.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/AudioContent.swift new file mode 100644 index 00000000000..4e730123153 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/AudioContent.swift @@ -0,0 +1,67 @@ +// 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. + +package import Foundation + +/// An audio content block. +package struct AudioContent: Codable, Sendable, Equatable, Hashable { + + /// The number of audio channels. + package let channels: Int? + + /// The audio content. + package let data: Data? + + /// The mime type of the audio. + package let mimeType: MimeType? + + /// The sample rate of the audio. + package let sampleRate: Int? + + package let type: String? + + /// The URI of the audio. + package let uri: String? + + /// Creates a new `AudioContent`. + /// + /// - Parameters: + /// - channels: The number of audio channels. + /// - data: The audio content. + /// - mimeType: The mime type of the audio. + /// - sampleRate: The sample rate of the audio. + /// - uri: The URI of the audio. + package init( + channels: Int? = nil, + data: Data? = nil, + mimeType: MimeType? = nil, + sampleRate: Int? = nil, + uri: String? = nil + ) { + self.channels = channels + self.data = data + self.mimeType = mimeType + self.sampleRate = sampleRate + self.type = "audio" + self.uri = uri + } + enum CodingKeys: String, CodingKey { + case channels = "channels" + case data = "data" + case mimeType = "mime_type" + case sampleRate = "sample_rate" + case type = "type" + case uri = "uri" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/AudioDelta+MimeType.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/AudioDelta+MimeType.swift new file mode 100644 index 00000000000..1ff0ba7531b --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/AudioDelta+MimeType.swift @@ -0,0 +1,104 @@ +// 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. + +extension AudioDelta { + package enum MimeType: Codable, Sendable, Equatable, Hashable { + + /// WAV audio format + case wav + + /// MP3 audio format + case mp3 + + /// AIFF audio format + case aiff + + /// AAC audio format + case aac + + /// OGG audio format + case ogg + + /// FLAC audio format + case flac + + /// MPEG audio format + case mpeg + + /// M4A audio format + case m4a + + /// L16 audio format + case l16 + + /// OPUS audio format + case opus + + /// ALAW audio format + case alaw + + /// MULAW audio format + case mulaw + + /// WEBM audio format + case webm + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension AudioDelta.MimeType: RawRepresentable { + package var rawValue: String { + switch self { + case .wav: "audio/wav" + case .mp3: "audio/mp3" + case .aiff: "audio/aiff" + case .aac: "audio/aac" + case .ogg: "audio/ogg" + case .flac: "audio/flac" + case .mpeg: "audio/mpeg" + case .m4a: "audio/m4a" + case .l16: "audio/l16" + case .opus: "audio/opus" + case .alaw: "audio/alaw" + case .mulaw: "audio/mulaw" + case .webm: "audio/webm" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "audio/wav": self = .wav + case "audio/mp3": self = .mp3 + case "audio/aiff": self = .aiff + case "audio/aac": self = .aac + case "audio/ogg": self = .ogg + case "audio/flac": self = .flac + case "audio/mpeg": self = .mpeg + case "audio/m4a": self = .m4a + case "audio/l16": self = .l16 + case "audio/opus": self = .opus + case "audio/alaw": self = .alaw + case "audio/mulaw": self = .mulaw + case "audio/webm": self = .webm + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/AudioDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/AudioDelta.swift new file mode 100644 index 00000000000..89e40eeb89a --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/AudioDelta.swift @@ -0,0 +1,72 @@ +// 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. + +package import Foundation + +/// An internal data model for `AudioDelta`. +package struct AudioDelta: Codable, Sendable, Equatable, Hashable { + + /// The number of audio channels. + package let channels: Int? + + package let data: Data? + + package let mimeType: MimeType? + + /// Deprecated. Use sample_rate instead. The value is ignored. + @available(*, deprecated) + package let rate: Int? + + /// The sample rate of the audio. + package let sampleRate: Int? + + package let type: String? + + package let uri: String? + + /// Creates a new `AudioDelta`. + /// + /// - Parameters: + /// - channels: The number of audio channels. + /// - data: For more details, see ``data``. + /// - mimeType: For more details, see ``mimeType``. + /// - rate: Deprecated. Use sample_rate instead. The value is ignored. + /// - sampleRate: The sample rate of the audio. + /// - uri: For more details, see ``uri``. + package init( + channels: Int? = nil, + data: Data? = nil, + mimeType: MimeType? = nil, + rate: Int? = nil, + sampleRate: Int? = nil, + uri: String? = nil + ) { + self.channels = channels + self.data = data + self.mimeType = mimeType + self.rate = rate + self.sampleRate = sampleRate + self.type = "audio" + self.uri = uri + } + enum CodingKeys: String, CodingKey { + case channels = "channels" + case data = "data" + case mimeType = "mime_type" + case rate = "rate" + case sampleRate = "sample_rate" + case type = "type" + case uri = "uri" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/AudioResponseFormat+Delivery.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/AudioResponseFormat+Delivery.swift new file mode 100644 index 00000000000..5ced904b0da --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/AudioResponseFormat+Delivery.swift @@ -0,0 +1,50 @@ +// 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. + +extension AudioResponseFormat { + /// The delivery mode for the audio output. + package enum Delivery: Codable, Sendable, Equatable, Hashable { + + /// Audio data is returned inline in the response. + case inline + + /// Audio data is returned as a URI. + case uri + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension AudioResponseFormat.Delivery: RawRepresentable { + package var rawValue: String { + switch self { + case .inline: "inline" + case .uri: "uri" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "inline": self = .inline + case "uri": self = .uri + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/AudioResponseFormat+MimeType.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/AudioResponseFormat+MimeType.swift new file mode 100644 index 00000000000..ec230abc3a4 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/AudioResponseFormat+MimeType.swift @@ -0,0 +1,70 @@ +// 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. + +extension AudioResponseFormat { + /// The MIME type of the audio output. + package enum MimeType: Codable, Sendable, Equatable, Hashable { + + /// MP3 audio format. + case mp3 + + /// OGG Opus audio format. + case oggOpus + + /// Raw PCM (L16) audio format. + case l16 + + /// WAV audio format. + case wav + + /// A-law audio format. + case alaw + + /// Mu-law audio format. + case mulaw + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension AudioResponseFormat.MimeType: RawRepresentable { + package var rawValue: String { + switch self { + case .mp3: "audio/mp3" + case .oggOpus: "audio/ogg_opus" + case .l16: "audio/l16" + case .wav: "audio/wav" + case .alaw: "audio/alaw" + case .mulaw: "audio/mulaw" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "audio/mp3": self = .mp3 + case "audio/ogg_opus": self = .oggOpus + case "audio/l16": self = .l16 + case "audio/wav": self = .wav + case "audio/alaw": self = .alaw + case "audio/mulaw": self = .mulaw + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/AudioResponseFormat.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/AudioResponseFormat.swift new file mode 100644 index 00000000000..65f08084784 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/AudioResponseFormat.swift @@ -0,0 +1,59 @@ +// 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. + +/// Configuration for audio output format. +package struct AudioResponseFormat: Codable, Sendable, Equatable, Hashable { + + /// Bit rate in bits per second (bps). Only applicable for compressed formats + /// (MP3, Opus). + package let bitRate: Int? + + /// The delivery mode for the audio output. + package let delivery: Delivery? + + /// The MIME type of the audio output. + package let mimeType: MimeType? + + /// Sample rate in Hz. + package let sampleRate: Int? + + package let type: String? + + /// Creates a new `AudioResponseFormat`. + /// + /// - Parameters: + /// - bitRate: Bit rate in bits per second (bps). Only applicable for compressed formats + /// - delivery: The delivery mode for the audio output. + /// - mimeType: The MIME type of the audio output. + /// - sampleRate: Sample rate in Hz. + package init( + bitRate: Int? = nil, + delivery: Delivery? = nil, + mimeType: MimeType? = nil, + sampleRate: Int? = nil + ) { + self.bitRate = bitRate + self.delivery = delivery + self.mimeType = mimeType + self.sampleRate = sampleRate + self.type = "audio" + } + enum CodingKeys: String, CodingKey { + case bitRate = "bit_rate" + case delivery = "delivery" + case mimeType = "mime_type" + case sampleRate = "sample_rate" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecution.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecution.swift new file mode 100644 index 00000000000..49aa2f9cf03 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecution.swift @@ -0,0 +1,28 @@ +// 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. + +/// A tool that can be used by the model to execute code. +package struct CodeExecution: Codable, Sendable, Equatable, Hashable { + + package let type: String? + + /// Creates a new `CodeExecution`. + /// + package init() { + self.type = "code_execution" + } + enum CodingKeys: String, CodingKey { + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionCallArguments+Language.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionCallArguments+Language.swift new file mode 100644 index 00000000000..89546c85038 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionCallArguments+Language.swift @@ -0,0 +1,45 @@ +// 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. + +extension CodeExecutionCallArguments { + /// Programming language of the `code`. + package enum Language: Codable, Sendable, Equatable, Hashable { + + /// Python >= 3.10, with numpy and simpy available. + case python + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension CodeExecutionCallArguments.Language: RawRepresentable { + package var rawValue: String { + switch self { + case .python: "python" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "python": self = .python + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionCallArguments.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionCallArguments.swift new file mode 100644 index 00000000000..68fb7f4cf3d --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionCallArguments.swift @@ -0,0 +1,38 @@ +// 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. + +/// The arguments to pass to the code execution. +package struct CodeExecutionCallArguments: Codable, Sendable, Equatable, Hashable { + + /// The code to be executed. + package let code: String? + + /// Programming language of the `code`. + package let language: String? + + /// Creates a new `CodeExecutionCallArguments`. + /// + /// - Parameters: + /// - code: The code to be executed. + package init( + code: String? = nil + ) { + self.code = code + self.language = "python" + } + enum CodingKeys: String, CodingKey { + case code = "code" + case language = "language" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionCallDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionCallDelta.swift new file mode 100644 index 00000000000..f439d396b4c --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionCallDelta.swift @@ -0,0 +1,45 @@ +// 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. + +package import Foundation + +/// An internal data model for `CodeExecutionCallDelta`. +package struct CodeExecutionCallDelta: Codable, Sendable, Equatable, Hashable { + + package let arguments: CodeExecutionCallArguments? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `CodeExecutionCallDelta`. + /// + /// - Parameters: + /// - arguments: For more details, see ``arguments``. + /// - signature: A signature hash for backend validation. + package init( + arguments: CodeExecutionCallArguments?, + signature: Data? = nil + ) { + self.arguments = arguments + self.signature = signature + self.type = "code_execution_call" + } + enum CodingKeys: String, CodingKey { + case arguments = "arguments" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionCallStep.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionCallStep.swift new file mode 100644 index 00000000000..75376685a4b --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionCallStep.swift @@ -0,0 +1,53 @@ +// 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. + +package import Foundation + +/// Code execution call step. +package struct CodeExecutionCallStep: Codable, Sendable, Equatable, Hashable { + + /// Required. The arguments to pass to the code execution. + package let arguments: CodeExecutionCallArguments? + + /// Required. A unique ID for this specific tool call. + package let id: String? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `CodeExecutionCallStep`. + /// + /// - Parameters: + /// - arguments: Required. The arguments to pass to the code execution. + /// - id: Required. A unique ID for this specific tool call. + /// - signature: A signature hash for backend validation. + package init( + arguments: CodeExecutionCallArguments?, + id: String?, + signature: Data? = nil + ) { + self.arguments = arguments + self.id = id + self.signature = signature + self.type = "code_execution_call" + } + enum CodingKeys: String, CodingKey { + case arguments = "arguments" + case id = "id" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionResultDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionResultDelta.swift new file mode 100644 index 00000000000..897fa3a0bee --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionResultDelta.swift @@ -0,0 +1,51 @@ +// 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. + +package import Foundation + +/// An internal data model for `CodeExecutionResultDelta`. +package struct CodeExecutionResultDelta: Codable, Sendable, Equatable, Hashable { + + package let isError: Bool? + + package let result: String? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `CodeExecutionResultDelta`. + /// + /// - Parameters: + /// - isError: For more details, see ``isError``. + /// - result: For more details, see ``result``. + /// - signature: A signature hash for backend validation. + package init( + isError: Bool? = nil, + result: String?, + signature: Data? = nil + ) { + self.isError = isError + self.result = result + self.signature = signature + self.type = "code_execution_result" + } + enum CodingKeys: String, CodingKey { + case isError = "is_error" + case result = "result" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionResultStep.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionResultStep.swift new file mode 100644 index 00000000000..8a6295b96dd --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/CodeExecutionResultStep.swift @@ -0,0 +1,60 @@ +// 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. + +package import Foundation + +/// Code execution result step. +package struct CodeExecutionResultStep: Codable, Sendable, Equatable, Hashable { + + /// Required. ID to match the ID from the function call block. + package let callId: String? + + /// Whether the code execution resulted in an error. + package let isError: Bool? + + /// Required. The output of the code execution. + package let result: String? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `CodeExecutionResultStep`. + /// + /// - Parameters: + /// - callId: Required. ID to match the ID from the function call block. + /// - isError: Whether the code execution resulted in an error. + /// - result: Required. The output of the code execution. + /// - signature: A signature hash for backend validation. + package init( + callId: String?, + isError: Bool? = nil, + result: String?, + signature: Data? = nil + ) { + self.callId = callId + self.isError = isError + self.result = result + self.signature = signature + self.type = "code_execution_result" + } + enum CodingKeys: String, CodingKey { + case callId = "call_id" + case isError = "is_error" + case result = "result" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ComputerUse+Environment.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ComputerUse+Environment.swift new file mode 100644 index 00000000000..e1d40653d7a --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ComputerUse+Environment.swift @@ -0,0 +1,55 @@ +// 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. + +extension ComputerUse { + /// The environment being operated. + package enum Environment: Codable, Sendable, Equatable, Hashable { + + /// Operates in a web browser. + case browser + + /// Operates in a mobile environment. + case mobile + + /// Operates in a desktop environment. + case desktop + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension ComputerUse.Environment: RawRepresentable { + package var rawValue: String { + switch self { + case .browser: "browser" + case .mobile: "mobile" + case .desktop: "desktop" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "browser": self = .browser + case "mobile": self = .mobile + case "desktop": self = .desktop + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ComputerUse.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ComputerUse.swift new file mode 100644 index 00000000000..7639bef0997 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ComputerUse.swift @@ -0,0 +1,59 @@ +// 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. + +/// A tool that can be used by the model to interact with the computer. +package struct ComputerUse: Codable, Sendable, Equatable, Hashable { + + /// Optional. Disabled safety policies for computer use. + package let disabledSafetyPolicies: [String]? + + /// Whether enable the prompt injection detection check on computer-use + /// request. + package let enablePromptInjectionDetection: Bool? + + /// The environment being operated. + package let environment: Environment? + + /// The list of predefined functions that are excluded from the model call. + package let excludedPredefinedFunctions: [String]? + + package let type: String? + + /// Creates a new `ComputerUse`. + /// + /// - Parameters: + /// - disabledSafetyPolicies: Optional. Disabled safety policies for computer use. + /// - enablePromptInjectionDetection: Whether enable the prompt injection detection check on computer-use + /// - environment: The environment being operated. + /// - excludedPredefinedFunctions: The list of predefined functions that are excluded from the model call. + package init( + disabledSafetyPolicies: [String]? = nil, + enablePromptInjectionDetection: Bool? = nil, + environment: Environment? = nil, + excludedPredefinedFunctions: [String]? = nil + ) { + self.disabledSafetyPolicies = disabledSafetyPolicies + self.enablePromptInjectionDetection = enablePromptInjectionDetection + self.environment = environment + self.excludedPredefinedFunctions = excludedPredefinedFunctions + self.type = "computer_use" + } + enum CodingKeys: String, CodingKey { + case disabledSafetyPolicies = "disabled_safety_policies" + case enablePromptInjectionDetection = "enable_prompt_injection_detection" + case environment = "environment" + case excludedPredefinedFunctions = "excluded_predefined_functions" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/Content.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/Content.swift new file mode 100644 index 00000000000..c2dbb573564 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/Content.swift @@ -0,0 +1,83 @@ +// 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. + +package import GeminiSharedDataModels + +/// The content of the response. +package enum Content: Codable, Sendable, Equatable, Hashable { + + /// An audio content block. + case audioContent(AudioContent) + + /// A document content block. + case documentContent(DocumentContent) + + /// An image content block. + case imageContent(ImageContent) + + /// A text content block. + case textContent(TextContent) + + /// A video content block. + case videoContent(VideoContent) + + /// Unrecognized case. + case unrecognized(JSONValue) + + private enum CodingKeys: String, CodingKey { + case discriminator = "type" + } + + package init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let discValue = try container.decode(String.self, forKey: .discriminator) + switch discValue { + case "audio": + let val = try AudioContent(from: decoder) + self = .audioContent(val) + case "document": + let val = try DocumentContent(from: decoder) + self = .documentContent(val) + case "image": + let val = try ImageContent(from: decoder) + self = .imageContent(val) + case "text": + let val = try TextContent(from: decoder) + self = .textContent(val) + case "video": + let val = try VideoContent(from: decoder) + self = .videoContent(val) + default: + let val = try JSONValue(from: decoder) + self = .unrecognized(val) + } + } + + package func encode(to encoder: any Encoder) throws { + switch self { + case .audioContent(let val): + try val.encode(to: encoder) + case .documentContent(let val): + try val.encode(to: encoder) + case .imageContent(let val): + try val.encode(to: encoder) + case .textContent(let val): + try val.encode(to: encoder) + case .videoContent(let val): + try val.encode(to: encoder) + case .unrecognized(let val): + try val.encode(to: encoder) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/CreateModelInteraction+Status.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/CreateModelInteraction+Status.swift new file mode 100644 index 00000000000..2b86a4a5994 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/CreateModelInteraction+Status.swift @@ -0,0 +1,81 @@ +// 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. + +extension CreateModelInteraction { + /// Required. Output only. The status of the interaction. + package enum Status: Codable, Sendable, Equatable, Hashable { + + /// The interaction is in progress. + case inProgress + + /// The interaction requires action/input from the user. + case requiresAction + + /// The interaction is completed. + case completed + + /// The interaction failed. + case failed + + /// The interaction was cancelled. + case cancelled + + /// The interaction is completed, but contains incomplete results (e.g. + /// hitting max_tokens). + case incomplete + + /// The interaction was halted because the token budget was exceeded. + case budgetExceeded + + /// The interaction is queued, waiting for processing. + case queued + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension CreateModelInteraction.Status: RawRepresentable { + package var rawValue: String { + switch self { + case .inProgress: "in_progress" + case .requiresAction: "requires_action" + case .completed: "completed" + case .failed: "failed" + case .cancelled: "cancelled" + case .incomplete: "incomplete" + case .budgetExceeded: "budget_exceeded" + case .queued: "queued" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "in_progress": self = .inProgress + case "requires_action": self = .requiresAction + case "completed": self = .completed + case "failed": self = .failed + case "cancelled": self = .cancelled + case "incomplete": self = .incomplete + case "budget_exceeded": self = .budgetExceeded + case "queued": self = .queued + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/CreateModelInteraction.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/CreateModelInteraction.swift new file mode 100644 index 00000000000..c930f7f5ea6 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/CreateModelInteraction.swift @@ -0,0 +1,187 @@ +// 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. + +package import GeminiSharedDataModels + +/// Parameters for creating model interactions +package struct CreateModelInteraction: Codable, Sendable, Equatable, Hashable { + + /// Input only. Whether to run the model interaction in the background. + package let background: Bool? + + /// Required. Output only. The time at which the response was created in ISO 8601 format + /// (YYYY-MM-DDThh:mm:ssZ). + package let created: String? + + /// The environment configuration for the interaction. Can be an object specifying remote environment sources or a string referencing an existing environment ID. + package let environment: JSONValue? + + /// Output only. The environment ID for the interaction. Only populated if environment + /// config is set in the request. + package let environmentId: String? + + /// Input only. Configuration parameters for the model interaction. + package let generationConfig: GenerationConfig? + + /// Required. Output only. A unique identifier for the interaction completion. + package let id: String? + + package let input: InteractionsInput? + + /// The labels with user-defined metadata for the request. + package let labels: [String: String]? + + /// The name of the `Model` used for generating the interaction. + package let model: Model? + + /// The ID of the previous interaction, if any. + package let previousInteractionId: String? + + /// Enforces that the generated response is a JSON object that complies with the JSON schema specified in this field. + package let responseFormat: JSONValue? + + /// The mime type of the response. This is required if response_format is set. + @available(*, deprecated) + package let responseMimeType: String? + + /// The requested modalities of the response (TEXT, IMAGE, AUDIO). + @available(*, deprecated) + package let responseModalities: [ResponseModality]? + + /// Safety settings for the interaction. + package let safetySettings: [SafetySetting]? + + /// The service tier for the interaction. + package let serviceTier: ServiceTier? + + /// Required. Output only. The status of the interaction. + package let status: Status? + + /// Input only. Whether to store the response and request for later retrieval. + package let store: Bool? + + /// Input only. Whether the interaction will be streamed. + package let stream: Bool? + + /// System instruction for the interaction. + package let systemInstruction: String? + + /// A list of tool declarations the model may call during interaction. + package let tools: [Tool]? + + /// Required. Output only. The time at which the response was last updated in ISO 8601 format + /// (YYYY-MM-DDThh:mm:ssZ). + package let updated: String? + + /// Optional. Webhook configuration for receiving notifications when the + /// interaction completes. + package let webhookConfig: WebhookConfig? + + /// Creates a new `CreateModelInteraction`. + /// + /// - Parameters: + /// - background: Input only. Whether to run the model interaction in the background. + /// - created: Required. Output only. The time at which the response was created in ISO 8601 format + /// - environment: The environment configuration for the interaction. Can be an object specifying remote environment sources or a string referencing an existing environment ID. + /// - environmentId: Output only. The environment ID for the interaction. Only populated if environment + /// - generationConfig: Input only. Configuration parameters for the model interaction. + /// - id: Required. Output only. A unique identifier for the interaction completion. + /// - input: For more details, see ``input``. + /// - labels: The labels with user-defined metadata for the request. + /// - model: The name of the `Model` used for generating the interaction. + /// - previousInteractionId: The ID of the previous interaction, if any. + /// - responseFormat: Enforces that the generated response is a JSON object that complies with the JSON schema specified in this field. + /// - responseMimeType: The mime type of the response. This is required if response_format is set. + /// - responseModalities: The requested modalities of the response (TEXT, IMAGE, AUDIO). + /// - safetySettings: Safety settings for the interaction. + /// - serviceTier: The service tier for the interaction. + /// - status: Required. Output only. The status of the interaction. + /// - store: Input only. Whether to store the response and request for later retrieval. + /// - stream: Input only. Whether the interaction will be streamed. + /// - systemInstruction: System instruction for the interaction. + /// - tools: A list of tool declarations the model may call during interaction. + /// - updated: Required. Output only. The time at which the response was last updated in ISO 8601 format + /// - webhookConfig: Optional. Webhook configuration for receiving notifications when the + package init( + background: Bool? = nil, + created: String? = nil, + environment: JSONValue? = nil, + environmentId: String? = nil, + generationConfig: GenerationConfig? = nil, + id: String? = nil, + input: InteractionsInput?, + labels: [String: String]? = nil, + model: Model?, + previousInteractionId: String? = nil, + responseFormat: JSONValue? = nil, + responseMimeType: String? = nil, + responseModalities: [ResponseModality]? = nil, + safetySettings: [SafetySetting]? = nil, + serviceTier: ServiceTier? = nil, + status: Status? = nil, + store: Bool? = nil, + stream: Bool? = nil, + systemInstruction: String? = nil, + tools: [Tool]? = nil, + updated: String? = nil, + webhookConfig: WebhookConfig? = nil + ) { + self.background = background + self.created = created + self.environment = environment + self.environmentId = environmentId + self.generationConfig = generationConfig + self.id = id + self.input = input + self.labels = labels + self.model = model + self.previousInteractionId = previousInteractionId + self.responseFormat = responseFormat + self.responseMimeType = responseMimeType + self.responseModalities = responseModalities + self.safetySettings = safetySettings + self.serviceTier = serviceTier + self.status = status + self.store = store + self.stream = stream + self.systemInstruction = systemInstruction + self.tools = tools + self.updated = updated + self.webhookConfig = webhookConfig + } + enum CodingKeys: String, CodingKey { + case background = "background" + case created = "created" + case environment = "environment" + case environmentId = "environment_id" + case generationConfig = "generation_config" + case id = "id" + case input = "input" + case labels = "labels" + case model = "model" + case previousInteractionId = "previous_interaction_id" + case responseFormat = "response_format" + case responseMimeType = "response_mime_type" + case responseModalities = "response_modalities" + case safetySettings = "safety_settings" + case serviceTier = "service_tier" + case status = "status" + case store = "store" + case stream = "stream" + case systemInstruction = "system_instruction" + case tools = "tools" + case updated = "updated" + case webhookConfig = "webhook_config" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/DeepResearchAgentConfig+Visualization.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/DeepResearchAgentConfig+Visualization.swift new file mode 100644 index 00000000000..7d972b46f8c --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/DeepResearchAgentConfig+Visualization.swift @@ -0,0 +1,50 @@ +// 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. + +extension DeepResearchAgentConfig { + /// Whether to include visualizations in the response. + package enum Visualization: Codable, Sendable, Equatable, Hashable { + + /// Do not include visualizations. + case off + + /// Automatically include visualizations. + case auto + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension DeepResearchAgentConfig.Visualization: RawRepresentable { + package var rawValue: String { + switch self { + case .off: "off" + case .auto: "auto" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "off": self = .off + case "auto": self = .auto + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/DeepResearchAgentConfig.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/DeepResearchAgentConfig.swift new file mode 100644 index 00000000000..2e138c87944 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/DeepResearchAgentConfig.swift @@ -0,0 +1,54 @@ +// 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. + +/// Configuration for the Deep Research agent. +package struct DeepResearchAgentConfig: Codable, Sendable, Equatable, Hashable { + + /// Enables human-in-the-loop planning for the Deep Research agent. If set to + /// true, the Deep Research agent will provide a research plan in its response. + /// The agent will then proceed only if the user confirms the plan in the next + /// turn. + package let collaborativePlanning: Bool? + + /// Whether to include thought summaries in the response. + package let thinkingSummaries: ThinkingSummaries? + + package let type: String? + + /// Whether to include visualizations in the response. + package let visualization: Visualization? + + /// Creates a new `DeepResearchAgentConfig`. + /// + /// - Parameters: + /// - collaborativePlanning: Enables human-in-the-loop planning for the Deep Research agent. If set to + /// - thinkingSummaries: Whether to include thought summaries in the response. + /// - visualization: Whether to include visualizations in the response. + package init( + collaborativePlanning: Bool? = nil, + thinkingSummaries: ThinkingSummaries? = nil, + visualization: Visualization? = nil + ) { + self.collaborativePlanning = collaborativePlanning + self.thinkingSummaries = thinkingSummaries + self.type = "deep-research" + self.visualization = visualization + } + enum CodingKeys: String, CodingKey { + case collaborativePlanning = "collaborative_planning" + case thinkingSummaries = "thinking_summaries" + case type = "type" + case visualization = "visualization" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/DocumentContent+MimeType.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/DocumentContent+MimeType.swift new file mode 100644 index 00000000000..a5160398d79 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/DocumentContent+MimeType.swift @@ -0,0 +1,50 @@ +// 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. + +extension DocumentContent { + /// The mime type of the document. + package enum MimeType: Codable, Sendable, Equatable, Hashable { + + /// PDF document format + case applicationPdf + + /// CSV document format + case textCsv + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension DocumentContent.MimeType: RawRepresentable { + package var rawValue: String { + switch self { + case .applicationPdf: "application/pdf" + case .textCsv: "text/csv" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "application/pdf": self = .applicationPdf + case "text/csv": self = .textCsv + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/DocumentContent.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/DocumentContent.swift new file mode 100644 index 00000000000..2d8c45d36cb --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/DocumentContent.swift @@ -0,0 +1,53 @@ +// 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. + +package import Foundation + +/// A document content block. +package struct DocumentContent: Codable, Sendable, Equatable, Hashable { + + /// The document content. + package let data: Data? + + /// The mime type of the document. + package let mimeType: MimeType? + + package let type: String? + + /// The URI of the document. + package let uri: String? + + /// Creates a new `DocumentContent`. + /// + /// - Parameters: + /// - data: The document content. + /// - mimeType: The mime type of the document. + /// - uri: The URI of the document. + package init( + data: Data? = nil, + mimeType: MimeType? = nil, + uri: String? = nil + ) { + self.data = data + self.mimeType = mimeType + self.type = "document" + self.uri = uri + } + enum CodingKeys: String, CodingKey { + case data = "data" + case mimeType = "mime_type" + case type = "type" + case uri = "uri" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/DocumentDelta+MimeType.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/DocumentDelta+MimeType.swift new file mode 100644 index 00000000000..50d2afb1a83 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/DocumentDelta+MimeType.swift @@ -0,0 +1,49 @@ +// 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. + +extension DocumentDelta { + package enum MimeType: Codable, Sendable, Equatable, Hashable { + + /// PDF document format + case applicationPdf + + /// CSV document format + case textCsv + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension DocumentDelta.MimeType: RawRepresentable { + package var rawValue: String { + switch self { + case .applicationPdf: "application/pdf" + case .textCsv: "text/csv" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "application/pdf": self = .applicationPdf + case "text/csv": self = .textCsv + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/DocumentDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/DocumentDelta.swift new file mode 100644 index 00000000000..162a2c06d50 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/DocumentDelta.swift @@ -0,0 +1,50 @@ +// 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. + +package import Foundation + +/// An internal data model for `DocumentDelta`. +package struct DocumentDelta: Codable, Sendable, Equatable, Hashable { + + package let data: Data? + + package let mimeType: MimeType? + + package let type: String? + + package let uri: String? + + /// Creates a new `DocumentDelta`. + /// + /// - Parameters: + /// - data: For more details, see ``data``. + /// - mimeType: For more details, see ``mimeType``. + /// - uri: For more details, see ``uri``. + package init( + data: Data? = nil, + mimeType: MimeType? = nil, + uri: String? = nil + ) { + self.data = data + self.mimeType = mimeType + self.type = "document" + self.uri = uri + } + enum CodingKeys: String, CodingKey { + case data = "data" + case mimeType = "mime_type" + case type = "type" + case uri = "uri" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/DynamicAgentConfig.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/DynamicAgentConfig.swift new file mode 100644 index 00000000000..9ac7b2bc901 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/DynamicAgentConfig.swift @@ -0,0 +1,28 @@ +// 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. + +/// Configuration for dynamic agents. +package struct DynamicAgentConfig: Codable, Sendable, Equatable, Hashable { + + package let type: String? + + /// Creates a new `DynamicAgentConfig`. + /// + package init() { + self.type = "dynamic" + } + enum CodingKeys: String, CodingKey { + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/Empty.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/Empty.swift new file mode 100644 index 00000000000..6edce34e41c --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/Empty.swift @@ -0,0 +1,28 @@ +// 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. + +/// A generic empty message that you can re-use to avoid defining duplicated +/// empty messages in your APIs. A typical example is to use it as the request +/// or the response type of an API method. For instance: +/// +/// service Foo { +/// rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); +/// } +package struct Empty: Codable, Sendable, Equatable, Hashable { + + /// Creates a new `Empty`. + /// + package init() { + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/Environment.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/Environment.swift new file mode 100644 index 00000000000..335c01ab0b0 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/Environment.swift @@ -0,0 +1,53 @@ +// 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. + +package import GeminiSharedDataModels + +/// Configuration for a custom environment. +package struct Environment: Codable, Sendable, Equatable, Hashable { + + /// Optional. The environment ID for the interaction. If specified, the request will + /// update the existing environment instead of creating a new one. + package let environmentId: String? + + /// Network configuration for the environment. + package let network: JSONValue? + + package let sources: [Source]? + + package let type: String? + + /// Creates a new `Environment`. + /// + /// - Parameters: + /// - environmentId: Optional. The environment ID for the interaction. If specified, the request will + /// - network: Network configuration for the environment. + /// - sources: For more details, see ``sources``. + package init( + environmentId: String? = nil, + network: JSONValue? = nil, + sources: [Source]? = nil + ) { + self.environmentId = environmentId + self.network = network + self.sources = sources + self.type = "remote" + } + enum CodingKeys: String, CodingKey { + case environmentId = "environment_id" + case network = "network" + case sources = "sources" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/EnvironmentNetworkEgressAllowlist.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/EnvironmentNetworkEgressAllowlist.swift new file mode 100644 index 00000000000..dcfbdc2f8e5 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/EnvironmentNetworkEgressAllowlist.swift @@ -0,0 +1,59 @@ +// 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. + +package import GeminiSharedDataModels + +package struct EnvironmentNetworkEgressAllowlistConfig: Codable, Sendable, Equatable, Hashable { + package let allowlist: [AllowlistEntry]? + package init(allowlist: [AllowlistEntry]?) { + self.allowlist = allowlist + } +} + +/// Outbound networking configuration for the sandbox. Accepts an object with an 'allowlist' array to restrict traffic, or the string 'disabled' to turn off all network access. Omit entirely to allow all outbound traffic with no header injection. +package enum EnvironmentNetworkEgressAllowlist: Codable, Sendable, Equatable, Hashable { + + case allowlist(EnvironmentNetworkEgressAllowlistConfig) + case disabled(String) + + package init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + if let val = try? container.decode(EnvironmentNetworkEgressAllowlistConfig.self) { + self = .allowlist(val) + return + } + if let val = try? container.decode(String.self) { + self = .disabled(val) + return + } + throw DecodingError.typeMismatch( + EnvironmentNetworkEgressAllowlist.self, + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: + "Failed to decode any of the variants for EnvironmentNetworkEgressAllowlist" + ) + ) + } + + package func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .allowlist(let val): + try container.encode(val) + case .disabled(let val): + try container.encode(val) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/Error.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/Error.swift new file mode 100644 index 00000000000..fe062f934e2 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/Error.swift @@ -0,0 +1,40 @@ +// 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. + +/// Error message from an interaction. +package struct Error: Codable, Sendable, Equatable, Hashable { + + /// A URI that identifies the error type. + package let code: String? + + /// A human-readable error message. + package let message: String? + + /// Creates a new `Error`. + /// + /// - Parameters: + /// - code: A URI that identifies the error type. + /// - message: A human-readable error message. + package init( + code: String? = nil, + message: String? = nil + ) { + self.code = code + self.message = message + } + enum CodingKeys: String, CodingKey { + case code = "code" + case message = "message" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ErrorEvent.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ErrorEvent.swift new file mode 100644 index 00000000000..e50fb7b6fb4 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ErrorEvent.swift @@ -0,0 +1,44 @@ +// 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. + +/// An internal data model for `ErrorEvent`. +package struct ErrorEvent: Codable, Sendable, Equatable, Hashable { + + package let error: Error? + + /// The event_id token to be used to resume the interaction stream, from + /// this event. + package let eventId: String? + + package let eventType: String? + + /// Creates a new `ErrorEvent`. + /// + /// - Parameters: + /// - error: For more details, see ``error``. + /// - eventId: The event_id token to be used to resume the interaction stream, from + package init( + error: Error? = nil, + eventId: String? = nil + ) { + self.error = error + self.eventId = eventId + self.eventType = "error" + } + enum CodingKeys: String, CodingKey { + case error = "error" + case eventId = "event_id" + case eventType = "event_type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/FileCitation.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/FileCitation.swift new file mode 100644 index 00000000000..56f1ed87308 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/FileCitation.swift @@ -0,0 +1,90 @@ +// 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. + +package import GeminiSharedDataModels + +/// A file citation annotation. +package struct FileCitation: Codable, Sendable, Equatable, Hashable { + + /// User provided metadata about the retrieved context. + package let customMetadata: [String: JSONValue]? + + /// The URI of the file. + package let documentUri: String? + + /// End of the attributed segment, exclusive. + package let endIndex: Int? + + /// The name of the file. + package let fileName: String? + + /// Media ID in-case of image citations, if applicable. + package let mediaId: String? + + /// Page number of the cited document, if applicable. + package let pageNumber: Int? + + /// Source attributed for a portion of the text. + package let source: String? + + /// Start of segment of the response that is attributed to this source. + /// + /// Index indicates the start of the segment, measured in bytes. + package let startIndex: Int? + + package let type: String? + + /// Creates a new `FileCitation`. + /// + /// - Parameters: + /// - customMetadata: User provided metadata about the retrieved context. + /// - documentUri: The URI of the file. + /// - endIndex: End of the attributed segment, exclusive. + /// - fileName: The name of the file. + /// - mediaId: Media ID in-case of image citations, if applicable. + /// - pageNumber: Page number of the cited document, if applicable. + /// - source: Source attributed for a portion of the text. + /// - startIndex: Start of segment of the response that is attributed to this source. + package init( + customMetadata: [String: JSONValue]? = nil, + documentUri: String? = nil, + endIndex: Int? = nil, + fileName: String? = nil, + mediaId: String? = nil, + pageNumber: Int? = nil, + source: String? = nil, + startIndex: Int? = nil + ) { + self.customMetadata = customMetadata + self.documentUri = documentUri + self.endIndex = endIndex + self.fileName = fileName + self.mediaId = mediaId + self.pageNumber = pageNumber + self.source = source + self.startIndex = startIndex + self.type = "file_citation" + } + enum CodingKeys: String, CodingKey { + case customMetadata = "custom_metadata" + case documentUri = "document_uri" + case endIndex = "end_index" + case fileName = "file_name" + case mediaId = "media_id" + case pageNumber = "page_number" + case source = "source" + case startIndex = "start_index" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/FileSearch.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/FileSearch.swift new file mode 100644 index 00000000000..d22c2ea21dc --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/FileSearch.swift @@ -0,0 +1,51 @@ +// 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. + +/// A tool that can be used by the model to search files. +package struct FileSearch: Codable, Sendable, Equatable, Hashable { + + /// The file search store names to search. + package let fileSearchStoreNames: [String]? + + /// Metadata filter to apply to the semantic retrieval documents and chunks. + package let metadataFilter: String? + + /// The number of semantic retrieval chunks to retrieve. + package let topK: Int? + + package let type: String? + + /// Creates a new `FileSearch`. + /// + /// - Parameters: + /// - fileSearchStoreNames: The file search store names to search. + /// - metadataFilter: Metadata filter to apply to the semantic retrieval documents and chunks. + /// - topK: The number of semantic retrieval chunks to retrieve. + package init( + fileSearchStoreNames: [String]? = nil, + metadataFilter: String? = nil, + topK: Int? = nil + ) { + self.fileSearchStoreNames = fileSearchStoreNames + self.metadataFilter = metadataFilter + self.topK = topK + self.type = "file_search" + } + enum CodingKeys: String, CodingKey { + case fileSearchStoreNames = "file_search_store_names" + case metadataFilter = "metadata_filter" + case topK = "top_k" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchCallDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchCallDelta.swift new file mode 100644 index 00000000000..9d8d9899f42 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchCallDelta.swift @@ -0,0 +1,39 @@ +// 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. + +package import Foundation + +/// An internal data model for `FileSearchCallDelta`. +package struct FileSearchCallDelta: Codable, Sendable, Equatable, Hashable { + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `FileSearchCallDelta`. + /// + /// - Parameters: + /// - signature: A signature hash for backend validation. + package init( + signature: Data? = nil + ) { + self.signature = signature + self.type = "file_search_call" + } + enum CodingKeys: String, CodingKey { + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchCallStep.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchCallStep.swift new file mode 100644 index 00000000000..9a3dc1b696d --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchCallStep.swift @@ -0,0 +1,46 @@ +// 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. + +package import Foundation + +/// File Search call step. +package struct FileSearchCallStep: Codable, Sendable, Equatable, Hashable { + + /// Required. A unique ID for this specific tool call. + package let id: String? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `FileSearchCallStep`. + /// + /// - Parameters: + /// - id: Required. A unique ID for this specific tool call. + /// - signature: A signature hash for backend validation. + package init( + id: String?, + signature: Data? = nil + ) { + self.id = id + self.signature = signature + self.type = "file_search_call" + } + enum CodingKeys: String, CodingKey { + case id = "id" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchResult.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchResult.swift new file mode 100644 index 00000000000..8416ecdcf49 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchResult.swift @@ -0,0 +1,22 @@ +// 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. + +/// The result of the File Search. +package struct FileSearchResult: Codable, Sendable, Equatable, Hashable { + + /// Creates a new `FileSearchResult`. + /// + package init() { + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchResultDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchResultDelta.swift new file mode 100644 index 00000000000..cdfa2d0ef41 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchResultDelta.swift @@ -0,0 +1,45 @@ +// 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. + +package import Foundation + +/// An internal data model for `FileSearchResultDelta`. +package struct FileSearchResultDelta: Codable, Sendable, Equatable, Hashable { + + package let result: [FileSearchResult]? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `FileSearchResultDelta`. + /// + /// - Parameters: + /// - result: For more details, see ``result``. + /// - signature: A signature hash for backend validation. + package init( + result: [FileSearchResult]?, + signature: Data? = nil + ) { + self.result = result + self.signature = signature + self.type = "file_search_result" + } + enum CodingKeys: String, CodingKey { + case result = "result" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchResultStep.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchResultStep.swift new file mode 100644 index 00000000000..51bd159f449 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/FileSearchResultStep.swift @@ -0,0 +1,46 @@ +// 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. + +package import Foundation + +/// File Search result step. +package struct FileSearchResultStep: Codable, Sendable, Equatable, Hashable { + + /// Required. ID to match the ID from the function call block. + package let callId: String? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `FileSearchResultStep`. + /// + /// - Parameters: + /// - callId: Required. ID to match the ID from the function call block. + /// - signature: A signature hash for backend validation. + package init( + callId: String?, + signature: Data? = nil + ) { + self.callId = callId + self.signature = signature + self.type = "file_search_result" + } + enum CodingKeys: String, CodingKey { + case callId = "call_id" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/Function.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/Function.swift new file mode 100644 index 00000000000..55dfaed4f2f --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/Function.swift @@ -0,0 +1,53 @@ +// 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. + +package import GeminiSharedDataModels + +/// A tool that can be used by the model. +package struct Function: Codable, Sendable, Equatable, Hashable { + + /// A description of the function. + package let description: String? + + /// The name of the function. + package let name: String? + + /// The JSON Schema for the function's parameters. + package let parameters: JSONValue? + + package let type: String? + + /// Creates a new `Function`. + /// + /// - Parameters: + /// - description: A description of the function. + /// - name: The name of the function. + /// - parameters: The JSON Schema for the function's parameters. + package init( + description: String? = nil, + name: String? = nil, + parameters: JSONValue? = nil + ) { + self.description = description + self.name = name + self.parameters = parameters + self.type = "function" + } + enum CodingKeys: String, CodingKey { + case description = "description" + case name = "name" + case parameters = "parameters" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/FunctionCallStep.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/FunctionCallStep.swift new file mode 100644 index 00000000000..5f10489c48b --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/FunctionCallStep.swift @@ -0,0 +1,53 @@ +// 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. + +package import GeminiSharedDataModels + +/// A function tool call step. +package struct FunctionCallStep: Codable, Sendable, Equatable, Hashable { + + /// Required. The arguments to pass to the function. + package let arguments: [String: JSONValue]? + + /// Required. A unique ID for this specific tool call. + package let id: String? + + /// Required. The name of the tool to call. + package let name: String? + + package let type: String? + + /// Creates a new `FunctionCallStep`. + /// + /// - Parameters: + /// - arguments: Required. The arguments to pass to the function. + /// - id: Required. A unique ID for this specific tool call. + /// - name: Required. The name of the tool to call. + package init( + arguments: [String: JSONValue]?, + id: String?, + name: String? + ) { + self.arguments = arguments + self.id = id + self.name = name + self.type = "function_call" + } + enum CodingKeys: String, CodingKey { + case arguments = "arguments" + case id = "id" + case name = "name" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/FunctionResultDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/FunctionResultDelta.swift new file mode 100644 index 00000000000..73264c7a814 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/FunctionResultDelta.swift @@ -0,0 +1,57 @@ +// 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. + +package import GeminiSharedDataModels + +/// An internal data model for `FunctionResultDelta`. +package struct FunctionResultDelta: Codable, Sendable, Equatable, Hashable { + + /// Required. ID to match the ID from the function call block. + package let callId: String? + + package let isError: Bool? + + package let name: String? + + package let result: JSONValue? + + package let type: String? + + /// Creates a new `FunctionResultDelta`. + /// + /// - Parameters: + /// - callId: Required. ID to match the ID from the function call block. + /// - isError: For more details, see ``isError``. + /// - name: For more details, see ``name``. + /// - result: For more details, see ``result``. + package init( + callId: String?, + isError: Bool? = nil, + name: String? = nil, + result: JSONValue? + ) { + self.callId = callId + self.isError = isError + self.name = name + self.result = result + self.type = "function_result" + } + enum CodingKeys: String, CodingKey { + case callId = "call_id" + case isError = "is_error" + case name = "name" + case result = "result" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/FunctionResultStep.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/FunctionResultStep.swift new file mode 100644 index 00000000000..a8d241f75d7 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/FunctionResultStep.swift @@ -0,0 +1,60 @@ +// 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. + +package import GeminiSharedDataModels + +/// Result of a function tool call. +package struct FunctionResultStep: Codable, Sendable, Equatable, Hashable { + + /// Required. ID to match the ID from the function call block. + package let callId: String? + + /// Whether the tool call resulted in an error. + package let isError: Bool? + + /// The name of the tool that was called. + package let name: String? + + /// Required. The result of the tool call. + package let result: JSONValue? + + package let type: String? + + /// Creates a new `FunctionResultStep`. + /// + /// - Parameters: + /// - callId: Required. ID to match the ID from the function call block. + /// - isError: Whether the tool call resulted in an error. + /// - name: The name of the tool that was called. + /// - result: Required. The result of the tool call. + package init( + callId: String?, + isError: Bool? = nil, + name: String? = nil, + result: JSONValue? + ) { + self.callId = callId + self.isError = isError + self.name = name + self.result = result + self.type = "function_result" + } + enum CodingKeys: String, CodingKey { + case callId = "call_id" + case isError = "is_error" + case name = "name" + case result = "result" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/GenerationConfig.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/GenerationConfig.swift new file mode 100644 index 00000000000..847577ca34a --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/GenerationConfig.swift @@ -0,0 +1,100 @@ +// 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. + +package import GeminiSharedDataModels + +/// Configuration parameters for model interactions. +package struct GenerationConfig: Codable, Sendable, Equatable, Hashable { + + /// Configuration for image interaction. + @available(*, deprecated) + package let imageConfig: ImageConfig? + + /// The maximum number of tokens to include in the response. + package let maxOutputTokens: Int? + + /// Seed used in decoding for reproducibility. + package let seed: Int? + + /// Optional. Speech and multi-speaker configuration. + package let speechConfig: JSONValue? + + /// A list of character sequences that will stop output interaction. + package let stopSequences: [String]? + + /// The level of thought tokens that the model should generate. + package let thinkingLevel: ThinkingLevel? + + /// Whether to include thought summaries in the response. + package let thinkingSummaries: ThinkingSummaries? + + /// The tool choice configuration. + package let toolChoice: JSONValue? + + /// Optional. Configuration for speech recognition (transcription). If present, ASR is + /// enabled. + package let transcriptionConfig: TranscriptionConfig? + + /// Configuration for video generation. + package let videoConfig: VideoConfig? + + /// Creates a new `GenerationConfig`. + /// + /// - Parameters: + /// - imageConfig: Configuration for image interaction. + /// - maxOutputTokens: The maximum number of tokens to include in the response. + /// - seed: Seed used in decoding for reproducibility. + /// - speechConfig: Optional. Speech and multi-speaker configuration. + /// - stopSequences: A list of character sequences that will stop output interaction. + /// - thinkingLevel: The level of thought tokens that the model should generate. + /// - thinkingSummaries: Whether to include thought summaries in the response. + /// - toolChoice: The tool choice configuration. + /// - transcriptionConfig: Optional. Configuration for speech recognition (transcription). If present, ASR is + /// - videoConfig: Configuration for video generation. + package init( + imageConfig: ImageConfig? = nil, + maxOutputTokens: Int? = nil, + seed: Int? = nil, + speechConfig: JSONValue? = nil, + stopSequences: [String]? = nil, + thinkingLevel: ThinkingLevel? = nil, + thinkingSummaries: ThinkingSummaries? = nil, + toolChoice: JSONValue? = nil, + transcriptionConfig: TranscriptionConfig? = nil, + videoConfig: VideoConfig? = nil + ) { + self.imageConfig = imageConfig + self.maxOutputTokens = maxOutputTokens + self.seed = seed + self.speechConfig = speechConfig + self.stopSequences = stopSequences + self.thinkingLevel = thinkingLevel + self.thinkingSummaries = thinkingSummaries + self.toolChoice = toolChoice + self.transcriptionConfig = transcriptionConfig + self.videoConfig = videoConfig + } + enum CodingKeys: String, CodingKey { + case imageConfig = "image_config" + case maxOutputTokens = "max_output_tokens" + case seed = "seed" + case speechConfig = "speech_config" + case stopSequences = "stop_sequences" + case thinkingLevel = "thinking_level" + case thinkingSummaries = "thinking_summaries" + case toolChoice = "tool_choice" + case transcriptionConfig = "transcription_config" + case videoConfig = "video_config" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMaps.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMaps.swift new file mode 100644 index 00000000000..2f5c0c4fc6c --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMaps.swift @@ -0,0 +1,52 @@ +// 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. + +/// A tool that can be used by the model to call Google Maps. +package struct GoogleMaps: Codable, Sendable, Equatable, Hashable { + + /// Whether to return a widget context token in the tool call result of the + /// response. + package let enableWidget: Bool? + + /// The latitude of the user's location. + package let latitude: Double? + + /// The longitude of the user's location. + package let longitude: Double? + + package let type: String? + + /// Creates a new `GoogleMaps`. + /// + /// - Parameters: + /// - enableWidget: Whether to return a widget context token in the tool call result of the + /// - latitude: The latitude of the user's location. + /// - longitude: The longitude of the user's location. + package init( + enableWidget: Bool? = nil, + latitude: Double? = nil, + longitude: Double? = nil + ) { + self.enableWidget = enableWidget + self.latitude = latitude + self.longitude = longitude + self.type = "google_maps" + } + enum CodingKeys: String, CodingKey { + case enableWidget = "enable_widget" + case latitude = "latitude" + case longitude = "longitude" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsCallArguments.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsCallArguments.swift new file mode 100644 index 00000000000..1a20ad9c18c --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsCallArguments.swift @@ -0,0 +1,33 @@ +// 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. + +/// The arguments to pass to the Google Maps tool. +package struct GoogleMapsCallArguments: Codable, Sendable, Equatable, Hashable { + + /// The queries to be executed. + package let queries: [String]? + + /// Creates a new `GoogleMapsCallArguments`. + /// + /// - Parameters: + /// - queries: The queries to be executed. + package init( + queries: [String]? = nil + ) { + self.queries = queries + } + enum CodingKeys: String, CodingKey { + case queries = "queries" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsCallDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsCallDelta.swift new file mode 100644 index 00000000000..07f9a3def33 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsCallDelta.swift @@ -0,0 +1,46 @@ +// 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. + +package import Foundation + +/// An internal data model for `GoogleMapsCallDelta`. +package struct GoogleMapsCallDelta: Codable, Sendable, Equatable, Hashable { + + /// The arguments to pass to the Google Maps tool. + package let arguments: GoogleMapsCallArguments? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `GoogleMapsCallDelta`. + /// + /// - Parameters: + /// - arguments: The arguments to pass to the Google Maps tool. + /// - signature: A signature hash for backend validation. + package init( + arguments: GoogleMapsCallArguments? = nil, + signature: Data? = nil + ) { + self.arguments = arguments + self.signature = signature + self.type = "google_maps_call" + } + enum CodingKeys: String, CodingKey { + case arguments = "arguments" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsCallStep.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsCallStep.swift new file mode 100644 index 00000000000..91fc4cb792e --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsCallStep.swift @@ -0,0 +1,53 @@ +// 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. + +package import Foundation + +/// Google Maps call step. +package struct GoogleMapsCallStep: Codable, Sendable, Equatable, Hashable { + + /// The arguments to pass to the Google Maps tool. + package let arguments: GoogleMapsCallArguments? + + /// Required. A unique ID for this specific tool call. + package let id: String? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `GoogleMapsCallStep`. + /// + /// - Parameters: + /// - arguments: The arguments to pass to the Google Maps tool. + /// - id: Required. A unique ID for this specific tool call. + /// - signature: A signature hash for backend validation. + package init( + arguments: GoogleMapsCallArguments? = nil, + id: String?, + signature: Data? = nil + ) { + self.arguments = arguments + self.id = id + self.signature = signature + self.type = "google_maps_call" + } + enum CodingKeys: String, CodingKey { + case arguments = "arguments" + case id = "id" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsResult.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsResult.swift new file mode 100644 index 00000000000..34a055facd8 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsResult.swift @@ -0,0 +1,38 @@ +// 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. + +/// The result of the Google Maps. +package struct GoogleMapsResult: Codable, Sendable, Equatable, Hashable { + + package let places: [GoogleMapsResultPlaces]? + + package let widgetContextToken: String? + + /// Creates a new `GoogleMapsResult`. + /// + /// - Parameters: + /// - places: For more details, see ``places``. + /// - widgetContextToken: For more details, see ``widgetContextToken``. + package init( + places: [GoogleMapsResultPlaces]? = nil, + widgetContextToken: String? = nil + ) { + self.places = places + self.widgetContextToken = widgetContextToken + } + enum CodingKeys: String, CodingKey { + case places = "places" + case widgetContextToken = "widget_context_token" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsResultDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsResultDelta.swift new file mode 100644 index 00000000000..da07c5ec824 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsResultDelta.swift @@ -0,0 +1,46 @@ +// 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. + +package import Foundation + +/// An internal data model for `GoogleMapsResultDelta`. +package struct GoogleMapsResultDelta: Codable, Sendable, Equatable, Hashable { + + /// The results of the Google Maps. + package let result: [GoogleMapsResult]? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `GoogleMapsResultDelta`. + /// + /// - Parameters: + /// - result: The results of the Google Maps. + /// - signature: A signature hash for backend validation. + package init( + result: [GoogleMapsResult]? = nil, + signature: Data? = nil + ) { + self.result = result + self.signature = signature + self.type = "google_maps_result" + } + enum CodingKeys: String, CodingKey { + case result = "result" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsResultPlaces.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsResultPlaces.swift new file mode 100644 index 00000000000..25aeed7d8fa --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsResultPlaces.swift @@ -0,0 +1,50 @@ +// 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. + +/// An internal data model for `GoogleMapsResultPlaces`. +package struct GoogleMapsResultPlaces: Codable, Sendable, Equatable, Hashable { + + package let name: String? + + package let placeId: String? + + package let reviewSnippets: [ReviewSnippet]? + + package let url: String? + + /// Creates a new `GoogleMapsResultPlaces`. + /// + /// - Parameters: + /// - name: For more details, see ``name``. + /// - placeId: For more details, see ``placeId``. + /// - reviewSnippets: For more details, see ``reviewSnippets``. + /// - url: For more details, see ``url``. + package init( + name: String? = nil, + placeId: String? = nil, + reviewSnippets: [ReviewSnippet]? = nil, + url: String? = nil + ) { + self.name = name + self.placeId = placeId + self.reviewSnippets = reviewSnippets + self.url = url + } + enum CodingKeys: String, CodingKey { + case name = "name" + case placeId = "place_id" + case reviewSnippets = "review_snippets" + case url = "url" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsResultStep.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsResultStep.swift new file mode 100644 index 00000000000..355fb343a6f --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleMapsResultStep.swift @@ -0,0 +1,52 @@ +// 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. + +package import Foundation + +/// Google Maps result step. +package struct GoogleMapsResultStep: Codable, Sendable, Equatable, Hashable { + + /// Required. ID to match the ID from the function call block. + package let callId: String? + + package let result: [GoogleMapsResult]? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `GoogleMapsResultStep`. + /// + /// - Parameters: + /// - callId: Required. ID to match the ID from the function call block. + /// - result: For more details, see ``result``. + /// - signature: A signature hash for backend validation. + package init( + callId: String?, + result: [GoogleMapsResult]?, + signature: Data? = nil + ) { + self.callId = callId + self.result = result + self.signature = signature + self.type = "google_maps_result" + } + enum CodingKeys: String, CodingKey { + case callId = "call_id" + case result = "result" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearch.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearch.swift new file mode 100644 index 00000000000..ab566478c8e --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearch.swift @@ -0,0 +1,37 @@ +// 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. + +/// A tool that can be used by the model to search Google. +package struct GoogleSearch: Codable, Sendable, Equatable, Hashable { + + /// The types of search grounding to enable. + package let searchTypes: [String]? + + package let type: String? + + /// Creates a new `GoogleSearch`. + /// + /// - Parameters: + /// - searchTypes: The types of search grounding to enable. + package init( + searchTypes: [String]? = nil + ) { + self.searchTypes = searchTypes + self.type = "google_search" + } + enum CodingKeys: String, CodingKey { + case searchTypes = "search_types" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchCallArguments.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchCallArguments.swift new file mode 100644 index 00000000000..59e8dfd5870 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchCallArguments.swift @@ -0,0 +1,33 @@ +// 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. + +/// The arguments to pass to Google Search. +package struct GoogleSearchCallArguments: Codable, Sendable, Equatable, Hashable { + + /// Web search queries for the following-up web search. + package let queries: [String]? + + /// Creates a new `GoogleSearchCallArguments`. + /// + /// - Parameters: + /// - queries: Web search queries for the following-up web search. + package init( + queries: [String]? = nil + ) { + self.queries = queries + } + enum CodingKeys: String, CodingKey { + case queries = "queries" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchCallDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchCallDelta.swift new file mode 100644 index 00000000000..d50763a97eb --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchCallDelta.swift @@ -0,0 +1,45 @@ +// 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. + +package import Foundation + +/// An internal data model for `GoogleSearchCallDelta`. +package struct GoogleSearchCallDelta: Codable, Sendable, Equatable, Hashable { + + package let arguments: GoogleSearchCallArguments? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `GoogleSearchCallDelta`. + /// + /// - Parameters: + /// - arguments: For more details, see ``arguments``. + /// - signature: A signature hash for backend validation. + package init( + arguments: GoogleSearchCallArguments?, + signature: Data? = nil + ) { + self.arguments = arguments + self.signature = signature + self.type = "google_search_call" + } + enum CodingKeys: String, CodingKey { + case arguments = "arguments" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchCallStep+SearchType.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchCallStep+SearchType.swift new file mode 100644 index 00000000000..2a092bab64c --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchCallStep+SearchType.swift @@ -0,0 +1,50 @@ +// 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. + +extension GoogleSearchCallStep { + /// The type of search grounding enabled. + package enum SearchType: Codable, Sendable, Equatable, Hashable { + + /// Setting this field enables web search. Only text results are returned. + case webSearch + + /// Setting this field enables image search. Image bytes are returned. + case imageSearch + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension GoogleSearchCallStep.SearchType: RawRepresentable { + package var rawValue: String { + switch self { + case .webSearch: "web_search" + case .imageSearch: "image_search" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "web_search": self = .webSearch + case "image_search": self = .imageSearch + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchCallStep.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchCallStep.swift new file mode 100644 index 00000000000..8b86d341251 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchCallStep.swift @@ -0,0 +1,60 @@ +// 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. + +package import Foundation + +/// Google Search call step. +package struct GoogleSearchCallStep: Codable, Sendable, Equatable, Hashable { + + /// Required. The arguments to pass to Google Search. + package let arguments: GoogleSearchCallArguments? + + /// Required. A unique ID for this specific tool call. + package let id: String? + + /// The type of search grounding enabled. + package let searchType: SearchType? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `GoogleSearchCallStep`. + /// + /// - Parameters: + /// - arguments: Required. The arguments to pass to Google Search. + /// - id: Required. A unique ID for this specific tool call. + /// - searchType: The type of search grounding enabled. + /// - signature: A signature hash for backend validation. + package init( + arguments: GoogleSearchCallArguments?, + id: String?, + searchType: SearchType? = nil, + signature: Data? = nil + ) { + self.arguments = arguments + self.id = id + self.searchType = searchType + self.signature = signature + self.type = "google_search_call" + } + enum CodingKeys: String, CodingKey { + case arguments = "arguments" + case id = "id" + case searchType = "search_type" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchResult.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchResult.swift new file mode 100644 index 00000000000..c0f8eb30eff --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchResult.swift @@ -0,0 +1,33 @@ +// 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. + +/// The result of the Google Search. +package struct GoogleSearchResult: Codable, Sendable, Equatable, Hashable { + + /// Web content snippet that can be embedded in a web page or an app webview. + package let searchSuggestions: String? + + /// Creates a new `GoogleSearchResult`. + /// + /// - Parameters: + /// - searchSuggestions: Web content snippet that can be embedded in a web page or an app webview. + package init( + searchSuggestions: String? = nil + ) { + self.searchSuggestions = searchSuggestions + } + enum CodingKeys: String, CodingKey { + case searchSuggestions = "search_suggestions" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchResultDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchResultDelta.swift new file mode 100644 index 00000000000..bd41b396167 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchResultDelta.swift @@ -0,0 +1,51 @@ +// 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. + +package import Foundation + +/// An internal data model for `GoogleSearchResultDelta`. +package struct GoogleSearchResultDelta: Codable, Sendable, Equatable, Hashable { + + package let isError: Bool? + + package let result: [GoogleSearchResult]? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `GoogleSearchResultDelta`. + /// + /// - Parameters: + /// - isError: For more details, see ``isError``. + /// - result: For more details, see ``result``. + /// - signature: A signature hash for backend validation. + package init( + isError: Bool? = nil, + result: [GoogleSearchResult]?, + signature: Data? = nil + ) { + self.isError = isError + self.result = result + self.signature = signature + self.type = "google_search_result" + } + enum CodingKeys: String, CodingKey { + case isError = "is_error" + case result = "result" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchResultStep.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchResultStep.swift new file mode 100644 index 00000000000..f42f3a51cd7 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/GoogleSearchResultStep.swift @@ -0,0 +1,60 @@ +// 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. + +package import Foundation + +/// Google Search result step. +package struct GoogleSearchResultStep: Codable, Sendable, Equatable, Hashable { + + /// Required. ID to match the ID from the function call block. + package let callId: String? + + /// Whether the Google Search resulted in an error. + package let isError: Bool? + + /// Required. The results of the Google Search. + package let result: [GoogleSearchResult]? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `GoogleSearchResultStep`. + /// + /// - Parameters: + /// - callId: Required. ID to match the ID from the function call block. + /// - isError: Whether the Google Search resulted in an error. + /// - result: Required. The results of the Google Search. + /// - signature: A signature hash for backend validation. + package init( + callId: String?, + isError: Bool? = nil, + result: [GoogleSearchResult]?, + signature: Data? = nil + ) { + self.callId = callId + self.isError = isError + self.result = result + self.signature = signature + self.type = "google_search_result" + } + enum CodingKeys: String, CodingKey { + case callId = "call_id" + case isError = "is_error" + case result = "result" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/GroundingToolCount+Type.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/GroundingToolCount+Type.swift new file mode 100644 index 00000000000..69dcfe6fcce --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/GroundingToolCount+Type.swift @@ -0,0 +1,51 @@ +// 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. + +extension GroundingToolCount { + /// The grounding tool type associated with the count. + package enum `Type`: Codable, Sendable, Equatable, Hashable { + + /// Grounding with Google Web Search and Image Search, & Web Grounding + /// for Enterprise. + case search + + /// Grounding with Google Maps. + case maps + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension GroundingToolCount.`Type`: RawRepresentable { + package var rawValue: String { + switch self { + case .search: "google_search" + case .maps: "google_maps" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "google_search": self = .search + case "google_maps": self = .maps + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/GroundingToolCount.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/GroundingToolCount.swift new file mode 100644 index 00000000000..f11af6207f5 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/GroundingToolCount.swift @@ -0,0 +1,40 @@ +// 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. + +/// The number of grounding tool counts. +package struct GroundingToolCount: Codable, Sendable, Equatable, Hashable { + + /// The number of grounding tool counts. + package let count: Int? + + /// The grounding tool type associated with the count. + package let type: `Type`? + + /// Creates a new `GroundingToolCount`. + /// + /// - Parameters: + /// - count: The number of grounding tool counts. + /// - type: The grounding tool type associated with the count. + package init( + count: Int? = nil, + type: `Type`? = nil + ) { + self.count = count + self.type = type + } + enum CodingKeys: String, CodingKey { + case count = "count" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/HarmCategory.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/HarmCategory.swift new file mode 100644 index 00000000000..e064d1ac2a2 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/HarmCategory.swift @@ -0,0 +1,90 @@ +// 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. + +/// An internal data model for `HarmCategory`. +package enum HarmCategory: Codable, Sendable, Equatable, Hashable { + + /// Content that promotes violence or incites hatred against individuals or + /// groups based on certain attributes. + case hateSpeech + + /// Content that promotes, facilitates, or enables dangerous activities. + case dangerousContent + + /// Abusive, threatening, or content intended to bully, torment, or ridicule. + case harassment + + /// Content that contains sexually explicit material. + case sexuallyExplicit + + /// Deprecated: Election filter is not longer supported. + /// The harm category is civic integrity. + case civicIntegrity + + /// Images that contain hate speech. + case imageHate + + /// Images that contain dangerous content. + case imageDangerousContent + + /// Images that contain harassment. + case imageHarassment + + /// Images that contain sexually explicit content. + case imageSexuallyExplicit + + /// Prompts designed to bypass safety filters. + case jailbreak + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) +} + +// MARK: - RawRepresentable Conformance + +extension HarmCategory: RawRepresentable { + package var rawValue: String { + switch self { + case .hateSpeech: "hate_speech" + case .dangerousContent: "dangerous_content" + case .harassment: "harassment" + case .sexuallyExplicit: "sexually_explicit" + case .civicIntegrity: "civic_integrity" + case .imageHate: "image_hate" + case .imageDangerousContent: "image_dangerous_content" + case .imageHarassment: "image_harassment" + case .imageSexuallyExplicit: "image_sexually_explicit" + case .jailbreak: "jailbreak" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "hate_speech": self = .hateSpeech + case "dangerous_content": self = .dangerousContent + case "harassment": self = .harassment + case "sexually_explicit": self = .sexuallyExplicit + case "civic_integrity": self = .civicIntegrity + case "image_hate": self = .imageHate + case "image_dangerous_content": self = .imageDangerousContent + case "image_harassment": self = .imageHarassment + case "image_sexually_explicit": self = .imageSexuallyExplicit + case "jailbreak": self = .jailbreak + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ImageConfig+AspectRatio.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageConfig+AspectRatio.swift new file mode 100644 index 00000000000..31c682e2f2d --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageConfig+AspectRatio.swift @@ -0,0 +1,109 @@ +// 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. + +extension ImageConfig { + package enum AspectRatio: Codable, Sendable, Equatable, Hashable { + + /// 1:1 aspect ratio. + case oneByOne + + /// 2:3 aspect ratio. + case twoByThree + + /// 3:2 aspect ratio. + case threeByTwo + + /// 3:4 aspect ratio. + case threeByFour + + /// 4:3 aspect ratio. + case fourByThree + + /// 4:5 aspect ratio. + case fourByFive + + /// 5:4 aspect ratio. + case fiveByFour + + /// 9:16 aspect ratio. + case nineBySixteen + + /// 16:9 aspect ratio. + case sixteenByNine + + /// 21:9 aspect ratio. + case twentyOneByNine + + /// 1:8 aspect ratio. + case oneByEight + + /// 8:1 aspect ratio. + case eightByOne + + /// 1:4 aspect ratio. + case oneByFour + + /// 4:1 aspect ratio. + case fourByOne + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension ImageConfig.AspectRatio: RawRepresentable { + package var rawValue: String { + switch self { + case .oneByOne: "1:1" + case .twoByThree: "2:3" + case .threeByTwo: "3:2" + case .threeByFour: "3:4" + case .fourByThree: "4:3" + case .fourByFive: "4:5" + case .fiveByFour: "5:4" + case .nineBySixteen: "9:16" + case .sixteenByNine: "16:9" + case .twentyOneByNine: "21:9" + case .oneByEight: "1:8" + case .eightByOne: "8:1" + case .oneByFour: "1:4" + case .fourByOne: "4:1" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "1:1": self = .oneByOne + case "2:3": self = .twoByThree + case "3:2": self = .threeByTwo + case "3:4": self = .threeByFour + case "4:3": self = .fourByThree + case "4:5": self = .fourByFive + case "5:4": self = .fiveByFour + case "9:16": self = .nineBySixteen + case "16:9": self = .sixteenByNine + case "21:9": self = .twentyOneByNine + case "1:8": self = .oneByEight + case "8:1": self = .eightByOne + case "1:4": self = .oneByFour + case "4:1": self = .fourByOne + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ImageConfig+ImageSize.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageConfig+ImageSize.swift new file mode 100644 index 00000000000..7d77fe93975 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageConfig+ImageSize.swift @@ -0,0 +1,59 @@ +// 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. + +extension ImageConfig { + package enum ImageSize: Codable, Sendable, Equatable, Hashable { + + /// 1K image size. + case oneK + + /// 2K image size. + case twoK + + /// 4K image size. + case fourK + + /// 512 image size. + case fiveTwelve + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension ImageConfig.ImageSize: RawRepresentable { + package var rawValue: String { + switch self { + case .oneK: "1K" + case .twoK: "2K" + case .fourK: "4K" + case .fiveTwelve: "512" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "1K": self = .oneK + case "2K": self = .twoK + case "4K": self = .fourK + case "512": self = .fiveTwelve + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ImageConfig.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageConfig.swift new file mode 100644 index 00000000000..8b47a31ee21 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageConfig.swift @@ -0,0 +1,39 @@ +// 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. + +/// The configuration for image interaction. +@available(*, deprecated) +package struct ImageConfig: Codable, Sendable, Equatable, Hashable { + + package let aspectRatio: AspectRatio? + + package let imageSize: ImageSize? + + /// Creates a new `ImageConfig`. + /// + /// - Parameters: + /// - aspectRatio: For more details, see ``aspectRatio``. + /// - imageSize: For more details, see ``imageSize``. + package init( + aspectRatio: AspectRatio? = nil, + imageSize: ImageSize? = nil + ) { + self.aspectRatio = aspectRatio + self.imageSize = imageSize + } + enum CodingKeys: String, CodingKey { + case aspectRatio = "aspect_ratio" + case imageSize = "image_size" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ImageContent+MimeType.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageContent+MimeType.swift new file mode 100644 index 00000000000..82f609037a1 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageContent+MimeType.swift @@ -0,0 +1,80 @@ +// 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. + +extension ImageContent { + /// The mime type of the image. + package enum MimeType: Codable, Sendable, Equatable, Hashable { + + /// PNG image format + case png + + /// JPEG image format + case jpeg + + /// WebP image format + case webp + + /// HEIC image format + case heic + + /// HEIF image format + case heif + + /// GIF image format + case gif + + /// BMP image format + case bmp + + /// TIFF image format + case tiff + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension ImageContent.MimeType: RawRepresentable { + package var rawValue: String { + switch self { + case .png: "image/png" + case .jpeg: "image/jpeg" + case .webp: "image/webp" + case .heic: "image/heic" + case .heif: "image/heif" + case .gif: "image/gif" + case .bmp: "image/bmp" + case .tiff: "image/tiff" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "image/png": self = .png + case "image/jpeg": self = .jpeg + case "image/webp": self = .webp + case "image/heic": self = .heic + case "image/heif": self = .heif + case "image/gif": self = .gif + case "image/bmp": self = .bmp + case "image/tiff": self = .tiff + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ImageContent.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageContent.swift new file mode 100644 index 00000000000..8453285a7a3 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageContent.swift @@ -0,0 +1,60 @@ +// 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. + +package import Foundation + +/// An image content block. +package struct ImageContent: Codable, Sendable, Equatable, Hashable { + + /// The image content. + package let data: Data? + + /// The mime type of the image. + package let mimeType: MimeType? + + /// The resolution of the media. + package let resolution: MediaResolution? + + package let type: String? + + /// The URI of the image. + package let uri: String? + + /// Creates a new `ImageContent`. + /// + /// - Parameters: + /// - data: The image content. + /// - mimeType: The mime type of the image. + /// - resolution: The resolution of the media. + /// - uri: The URI of the image. + package init( + data: Data? = nil, + mimeType: MimeType? = nil, + resolution: MediaResolution? = nil, + uri: String? = nil + ) { + self.data = data + self.mimeType = mimeType + self.resolution = resolution + self.type = "image" + self.uri = uri + } + enum CodingKeys: String, CodingKey { + case data = "data" + case mimeType = "mime_type" + case resolution = "resolution" + case type = "type" + case uri = "uri" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ImageDelta+MimeType.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageDelta+MimeType.swift new file mode 100644 index 00000000000..0fc72eda918 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageDelta+MimeType.swift @@ -0,0 +1,79 @@ +// 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. + +extension ImageDelta { + package enum MimeType: Codable, Sendable, Equatable, Hashable { + + /// PNG image format + case png + + /// JPEG image format + case jpeg + + /// WebP image format + case webp + + /// HEIC image format + case heic + + /// HEIF image format + case heif + + /// GIF image format + case gif + + /// BMP image format + case bmp + + /// TIFF image format + case tiff + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension ImageDelta.MimeType: RawRepresentable { + package var rawValue: String { + switch self { + case .png: "image/png" + case .jpeg: "image/jpeg" + case .webp: "image/webp" + case .heic: "image/heic" + case .heif: "image/heif" + case .gif: "image/gif" + case .bmp: "image/bmp" + case .tiff: "image/tiff" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "image/png": self = .png + case "image/jpeg": self = .jpeg + case "image/webp": self = .webp + case "image/heic": self = .heic + case "image/heif": self = .heif + case "image/gif": self = .gif + case "image/bmp": self = .bmp + case "image/tiff": self = .tiff + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ImageDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageDelta.swift new file mode 100644 index 00000000000..bb9dc378b59 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageDelta.swift @@ -0,0 +1,57 @@ +// 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. + +package import Foundation + +/// An internal data model for `ImageDelta`. +package struct ImageDelta: Codable, Sendable, Equatable, Hashable { + + package let data: Data? + + package let mimeType: MimeType? + + /// The resolution of the media. + package let resolution: MediaResolution? + + package let type: String? + + package let uri: String? + + /// Creates a new `ImageDelta`. + /// + /// - Parameters: + /// - data: For more details, see ``data``. + /// - mimeType: For more details, see ``mimeType``. + /// - resolution: The resolution of the media. + /// - uri: For more details, see ``uri``. + package init( + data: Data? = nil, + mimeType: MimeType? = nil, + resolution: MediaResolution? = nil, + uri: String? = nil + ) { + self.data = data + self.mimeType = mimeType + self.resolution = resolution + self.type = "image" + self.uri = uri + } + enum CodingKeys: String, CodingKey { + case data = "data" + case mimeType = "mime_type" + case resolution = "resolution" + case type = "type" + case uri = "uri" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat+AspectRatio.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat+AspectRatio.swift new file mode 100644 index 00000000000..81a7a9184ae --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat+AspectRatio.swift @@ -0,0 +1,110 @@ +// 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. + +extension ImageResponseFormat { + /// The aspect ratio for the image output. + package enum AspectRatio: Codable, Sendable, Equatable, Hashable { + + /// 1:1 aspect ratio. + case oneByOne + + /// 2:3 aspect ratio. + case twoByThree + + /// 3:2 aspect ratio. + case threeByTwo + + /// 3:4 aspect ratio. + case threeByFour + + /// 4:3 aspect ratio. + case fourByThree + + /// 4:5 aspect ratio. + case fourByFive + + /// 5:4 aspect ratio. + case fiveByFour + + /// 9:16 aspect ratio. + case nineBySixteen + + /// 16:9 aspect ratio. + case sixteenByNine + + /// 21:9 aspect ratio. + case twentyOneByNine + + /// 1:8 aspect ratio. + case oneByEight + + /// 8:1 aspect ratio. + case eightByOne + + /// 1:4 aspect ratio. + case oneByFour + + /// 4:1 aspect ratio. + case fourByOne + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension ImageResponseFormat.AspectRatio: RawRepresentable { + package var rawValue: String { + switch self { + case .oneByOne: "1:1" + case .twoByThree: "2:3" + case .threeByTwo: "3:2" + case .threeByFour: "3:4" + case .fourByThree: "4:3" + case .fourByFive: "4:5" + case .fiveByFour: "5:4" + case .nineBySixteen: "9:16" + case .sixteenByNine: "16:9" + case .twentyOneByNine: "21:9" + case .oneByEight: "1:8" + case .eightByOne: "8:1" + case .oneByFour: "1:4" + case .fourByOne: "4:1" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "1:1": self = .oneByOne + case "2:3": self = .twoByThree + case "3:2": self = .threeByTwo + case "3:4": self = .threeByFour + case "4:3": self = .fourByThree + case "4:5": self = .fourByFive + case "5:4": self = .fiveByFour + case "9:16": self = .nineBySixteen + case "16:9": self = .sixteenByNine + case "21:9": self = .twentyOneByNine + case "1:8": self = .oneByEight + case "8:1": self = .eightByOne + case "1:4": self = .oneByFour + case "4:1": self = .fourByOne + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat+Delivery.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat+Delivery.swift new file mode 100644 index 00000000000..f88662d2489 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat+Delivery.swift @@ -0,0 +1,50 @@ +// 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. + +extension ImageResponseFormat { + /// The delivery mode for the image output. + package enum Delivery: Codable, Sendable, Equatable, Hashable { + + /// Image data is returned inline in the response. + case inline + + /// Image data is returned as a URI. + case uri + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension ImageResponseFormat.Delivery: RawRepresentable { + package var rawValue: String { + switch self { + case .inline: "inline" + case .uri: "uri" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "inline": self = .inline + case "uri": self = .uri + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat+ImageSize.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat+ImageSize.swift new file mode 100644 index 00000000000..d91cb71667d --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat+ImageSize.swift @@ -0,0 +1,60 @@ +// 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. + +extension ImageResponseFormat { + /// The size of the image output. + package enum ImageSize: Codable, Sendable, Equatable, Hashable { + + /// 512px image size. + case fiveTwelve + + /// 1K image size. + case oneK + + /// 2K image size. + case twoK + + /// 4K image size. + case fourK + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension ImageResponseFormat.ImageSize: RawRepresentable { + package var rawValue: String { + switch self { + case .fiveTwelve: "512" + case .oneK: "1K" + case .twoK: "2K" + case .fourK: "4K" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "512": self = .fiveTwelve + case "1K": self = .oneK + case "2K": self = .twoK + case "4K": self = .fourK + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat+MimeType.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat+MimeType.swift new file mode 100644 index 00000000000..8382db9ae49 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat+MimeType.swift @@ -0,0 +1,45 @@ +// 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. + +extension ImageResponseFormat { + /// The MIME type of the image output. + package enum MimeType: Codable, Sendable, Equatable, Hashable { + + /// JPEG image format. + case jpeg + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension ImageResponseFormat.MimeType: RawRepresentable { + package var rawValue: String { + switch self { + case .jpeg: "image/jpeg" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "image/jpeg": self = .jpeg + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat.swift new file mode 100644 index 00000000000..d77c31ca34f --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ImageResponseFormat.swift @@ -0,0 +1,56 @@ +// 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. + +/// Configuration for image output format. +package struct ImageResponseFormat: Codable, Sendable, Equatable, Hashable { + + /// The aspect ratio for the image output. + package let aspectRatio: AspectRatio? + + /// The delivery mode for the image output. + package let delivery: Delivery? + + /// The size of the image output. + package let imageSize: ImageSize? + + /// The MIME type of the image output. + package let mimeType: String? + + package let type: String? + + /// Creates a new `ImageResponseFormat`. + /// + /// - Parameters: + /// - aspectRatio: The aspect ratio for the image output. + /// - delivery: The delivery mode for the image output. + /// - imageSize: The size of the image output. + package init( + aspectRatio: AspectRatio? = nil, + delivery: Delivery? = nil, + imageSize: ImageSize? = nil + ) { + self.aspectRatio = aspectRatio + self.delivery = delivery + self.imageSize = imageSize + self.mimeType = "image/jpeg" + self.type = "image" + } + enum CodingKeys: String, CodingKey { + case aspectRatio = "aspect_ratio" + case delivery = "delivery" + case imageSize = "image_size" + case mimeType = "mime_type" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/Interaction+Status.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/Interaction+Status.swift new file mode 100644 index 00000000000..f0e21f00c89 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/Interaction+Status.swift @@ -0,0 +1,81 @@ +// 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. + +extension Interaction { + /// Required. Output only. The status of the interaction. + package enum Status: Codable, Sendable, Equatable, Hashable { + + /// The interaction is in progress. + case inProgress + + /// The interaction requires action/input from the user. + case requiresAction + + /// The interaction is completed. + case completed + + /// The interaction failed. + case failed + + /// The interaction was cancelled. + case cancelled + + /// The interaction is completed, but contains incomplete results (e.g. + /// hitting max_tokens). + case incomplete + + /// The interaction was halted because the token budget was exceeded. + case budgetExceeded + + /// The interaction is queued, waiting for processing. + case queued + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension Interaction.Status: RawRepresentable { + package var rawValue: String { + switch self { + case .inProgress: "in_progress" + case .requiresAction: "requires_action" + case .completed: "completed" + case .failed: "failed" + case .cancelled: "cancelled" + case .incomplete: "incomplete" + case .budgetExceeded: "budget_exceeded" + case .queued: "queued" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "in_progress": self = .inProgress + case "requires_action": self = .requiresAction + case "completed": self = .completed + case "failed": self = .failed + case "cancelled": self = .cancelled + case "incomplete": self = .incomplete + case "budget_exceeded": self = .budgetExceeded + case "queued": self = .queued + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/Interaction.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/Interaction.swift new file mode 100644 index 00000000000..5747c5c59bc --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/Interaction.swift @@ -0,0 +1,201 @@ +// 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. + +package import GeminiSharedDataModels + +/// The Interaction resource. +package struct Interaction: Codable, Sendable, Equatable, Hashable { + + /// The name of the `Agent` used for generating the interaction. + package let agent: AgentOption? + + /// Configuration parameters for the agent interaction. + package let agentConfig: JSONValue? + + /// Output only. The time at which the response was created in ISO 8601 format + /// (YYYY-MM-DDThh:mm:ssZ). + package let created: String? + + /// The environment configuration for the interaction. Can be an object specifying remote environment sources or a string referencing an existing environment ID. + package let environment: JSONValue? + + /// Output only. The environment ID for the interaction. Only populated if environment + /// config is set in the request. + package let environmentId: String? + + /// Output only. Diagnostic faults / platform errors recorded on the interaction. + package let errors: [Error]? + + /// Input only. Configuration parameters for the model interaction. + package let generationConfig: GenerationConfig? + + /// Required. Output only. A unique identifier for the interaction completion. + package let id: String? + + package let input: InteractionsInput? + + /// The labels with user-defined metadata for the request. + package let labels: [String: String]? + + /// The name of the `Model` used for generating the interaction. + package let model: Model? + + /// The ID of the previous interaction, if any. + package let previousInteractionId: String? + + /// Enforces that the generated response is a JSON object that complies with the JSON schema specified in this field. + package let responseFormat: JSONValue? + + /// The mime type of the response. This is required if response_format is set. + @available(*, deprecated) + package let responseMimeType: String? + + /// The requested modalities of the response (TEXT, IMAGE, AUDIO). + @available(*, deprecated) + package let responseModalities: [ResponseModality]? + + /// Safety settings for the interaction. + package let safetySettings: [SafetySetting]? + + /// The service tier for the interaction. + package let serviceTier: ServiceTier? + + /// Required. Output only. The status of the interaction. + package let status: Status? + + /// Output only. The steps that make up the interaction, when included in the response. + package let steps: [Step]? + + /// System instruction for the interaction. + package let systemInstruction: String? + + /// A list of tool declarations the model may call during interaction. + package let tools: [Tool]? + + /// Output only. The time at which the response was last updated in ISO 8601 format + /// (YYYY-MM-DDThh:mm:ssZ). + package let updated: String? + + /// Output only. Statistics on the interaction request's token usage. + package let usage: Usage? + + /// Optional. Webhook configuration for receiving notifications when the + /// interaction completes. + package let webhookConfig: WebhookConfig? + + /// Creates a new `Interaction`. + /// + /// - Parameters: + /// - agent: The name of the `Agent` used for generating the interaction. + /// - agentConfig: Configuration parameters for the agent interaction. + /// - created: Output only. The time at which the response was created in ISO 8601 format + /// - environment: The environment configuration for the interaction. Can be an object specifying remote environment sources or a string referencing an existing environment ID. + /// - environmentId: Output only. The environment ID for the interaction. Only populated if environment + /// - errors: Output only. Diagnostic faults / platform errors recorded on the interaction. + /// - generationConfig: Input only. Configuration parameters for the model interaction. + /// - id: Required. Output only. A unique identifier for the interaction completion. + /// - input: For more details, see ``input``. + /// - labels: The labels with user-defined metadata for the request. + /// - model: The name of the `Model` used for generating the interaction. + /// - previousInteractionId: The ID of the previous interaction, if any. + /// - responseFormat: Enforces that the generated response is a JSON object that complies with the JSON schema specified in this field. + /// - responseMimeType: The mime type of the response. This is required if response_format is set. + /// - responseModalities: The requested modalities of the response (TEXT, IMAGE, AUDIO). + /// - safetySettings: Safety settings for the interaction. + /// - serviceTier: The service tier for the interaction. + /// - status: Required. Output only. The status of the interaction. + /// - steps: Output only. The steps that make up the interaction, when included in the response. + /// - systemInstruction: System instruction for the interaction. + /// - tools: A list of tool declarations the model may call during interaction. + /// - updated: Output only. The time at which the response was last updated in ISO 8601 format + /// - usage: Output only. Statistics on the interaction request's token usage. + /// - webhookConfig: Optional. Webhook configuration for receiving notifications when the + package init( + agent: AgentOption? = nil, + agentConfig: JSONValue? = nil, + created: String? = nil, + environment: JSONValue? = nil, + environmentId: String? = nil, + errors: [Error]? = nil, + generationConfig: GenerationConfig? = nil, + id: String? = nil, + input: InteractionsInput? = nil, + labels: [String: String]? = nil, + model: Model? = nil, + previousInteractionId: String? = nil, + responseFormat: JSONValue? = nil, + responseMimeType: String? = nil, + responseModalities: [ResponseModality]? = nil, + safetySettings: [SafetySetting]? = nil, + serviceTier: ServiceTier? = nil, + status: Status?, + steps: [Step]? = nil, + systemInstruction: String? = nil, + tools: [Tool]? = nil, + updated: String? = nil, + usage: Usage? = nil, + webhookConfig: WebhookConfig? = nil + ) { + self.agent = agent + self.agentConfig = agentConfig + self.created = created + self.environment = environment + self.environmentId = environmentId + self.errors = errors + self.generationConfig = generationConfig + self.id = id + self.input = input + self.labels = labels + self.model = model + self.previousInteractionId = previousInteractionId + self.responseFormat = responseFormat + self.responseMimeType = responseMimeType + self.responseModalities = responseModalities + self.safetySettings = safetySettings + self.serviceTier = serviceTier + self.status = status + self.steps = steps + self.systemInstruction = systemInstruction + self.tools = tools + self.updated = updated + self.usage = usage + self.webhookConfig = webhookConfig + } + enum CodingKeys: String, CodingKey { + case agent = "agent" + case agentConfig = "agent_config" + case created = "created" + case environment = "environment" + case environmentId = "environment_id" + case errors = "errors" + case generationConfig = "generation_config" + case id = "id" + case input = "input" + case labels = "labels" + case model = "model" + case previousInteractionId = "previous_interaction_id" + case responseFormat = "response_format" + case responseMimeType = "response_mime_type" + case responseModalities = "response_modalities" + case safetySettings = "safety_settings" + case serviceTier = "service_tier" + case status = "status" + case steps = "steps" + case systemInstruction = "system_instruction" + case tools = "tools" + case updated = "updated" + case usage = "usage" + case webhookConfig = "webhook_config" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionCompletedEvent.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionCompletedEvent.swift new file mode 100644 index 00000000000..9824b16a1b3 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionCompletedEvent.swift @@ -0,0 +1,45 @@ +// 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. + +/// An internal data model for `InteractionCompletedEvent`. +package struct InteractionCompletedEvent: Codable, Sendable, Equatable, Hashable { + + /// The event_id token to be used to resume the interaction stream, from + /// this event. + package let eventId: String? + + package let eventType: String? + + /// Partial completed interaction resource emitted at the end of the stream. + package let interaction: InteractionSseEventInteraction? + + /// Creates a new `InteractionCompletedEvent`. + /// + /// - Parameters: + /// - eventId: The event_id token to be used to resume the interaction stream, from + /// - interaction: Partial completed interaction resource emitted at the end of the stream. + package init( + eventId: String? = nil, + interaction: InteractionSseEventInteraction? + ) { + self.eventId = eventId + self.eventType = "interaction.completed" + self.interaction = interaction + } + enum CodingKeys: String, CodingKey { + case eventId = "event_id" + case eventType = "event_type" + case interaction = "interaction" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionCreatedEvent.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionCreatedEvent.swift new file mode 100644 index 00000000000..862d955c670 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionCreatedEvent.swift @@ -0,0 +1,45 @@ +// 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. + +/// An internal data model for `InteractionCreatedEvent`. +package struct InteractionCreatedEvent: Codable, Sendable, Equatable, Hashable { + + /// The event_id token to be used to resume the interaction stream, from + /// this event. + package let eventId: String? + + package let eventType: String? + + /// Partial interaction resource emitted when the stream is created. + package let interaction: InteractionSseEventInteraction? + + /// Creates a new `InteractionCreatedEvent`. + /// + /// - Parameters: + /// - eventId: The event_id token to be used to resume the interaction stream, from + /// - interaction: Partial interaction resource emitted when the stream is created. + package init( + eventId: String? = nil, + interaction: InteractionSseEventInteraction? + ) { + self.eventId = eventId + self.eventType = "interaction.created" + self.interaction = interaction + } + enum CodingKeys: String, CodingKey { + case eventId = "event_id" + case eventType = "event_type" + case interaction = "interaction" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionSSEEvent.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionSSEEvent.swift new file mode 100644 index 00000000000..53740c142b9 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionSSEEvent.swift @@ -0,0 +1,102 @@ +// 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. + +package import GeminiSharedDataModels + +/// An internal data model for `InteractionSSEEvent`. +package enum InteractionSSEEvent: Codable, Sendable, Equatable, Hashable { + + /// An internal data model for `ErrorEvent`. + case errorEvent(ErrorEvent) + + /// An internal data model for `InteractionCompletedEvent`. + case interactionCompletedEvent(InteractionCompletedEvent) + + /// An internal data model for `InteractionCreatedEvent`. + case interactionCreatedEvent(InteractionCreatedEvent) + + /// An internal data model for `InteractionStatusUpdate`. + case interactionStatusUpdate(InteractionStatusUpdate) + + /// An internal data model for `StepDelta`. + case stepDelta(StepDelta) + + /// An internal data model for `StepStart`. + case stepStart(StepStart) + + /// An internal data model for `StepStop`. + case stepStop(StepStop) + + /// Unrecognized case. + case unrecognized(JSONValue) + + private enum CodingKeys: String, CodingKey { + case discriminator = "event_type" + case typeFallback = "type" + } + + package init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let discValue = + try container.decodeIfPresent(String.self, forKey: .discriminator) + ?? container.decode(String.self, forKey: .typeFallback) + switch discValue { + case "error": + let val = try ErrorEvent(from: decoder) + self = .errorEvent(val) + case "interaction.completed": + let val = try InteractionCompletedEvent(from: decoder) + self = .interactionCompletedEvent(val) + case "interaction.created": + let val = try InteractionCreatedEvent(from: decoder) + self = .interactionCreatedEvent(val) + case "interaction.status_update": + let val = try InteractionStatusUpdate(from: decoder) + self = .interactionStatusUpdate(val) + case "step.delta": + let val = try StepDelta(from: decoder) + self = .stepDelta(val) + case "step.start": + let val = try StepStart(from: decoder) + self = .stepStart(val) + case "step.stop": + let val = try StepStop(from: decoder) + self = .stepStop(val) + default: + let val = try JSONValue(from: decoder) + self = .unrecognized(val) + } + } + + package func encode(to encoder: any Encoder) throws { + switch self { + case .errorEvent(let val): + try val.encode(to: encoder) + case .interactionCompletedEvent(let val): + try val.encode(to: encoder) + case .interactionCreatedEvent(let val): + try val.encode(to: encoder) + case .interactionStatusUpdate(let val): + try val.encode(to: encoder) + case .stepDelta(let val): + try val.encode(to: encoder) + case .stepStart(let val): + try val.encode(to: encoder) + case .stepStop(let val): + try val.encode(to: encoder) + case .unrecognized(let val): + try val.encode(to: encoder) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionSSEStreamEvent.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionSSEStreamEvent.swift new file mode 100644 index 00000000000..e6c9b1b7aab --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionSSEStreamEvent.swift @@ -0,0 +1,32 @@ +// 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. + +/// An internal data model for `InteractionSSEStreamEvent`. +package struct InteractionSSEStreamEvent: Codable, Sendable, Equatable, Hashable { + + package let data: InteractionSSEEvent? + + /// Creates a new `InteractionSSEStreamEvent`. + /// + /// - Parameters: + /// - data: For more details, see ``data``. + package init( + data: InteractionSSEEvent? + ) { + self.data = data + } + enum CodingKeys: String, CodingKey { + case data = "data" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionSseEventInteraction+Status.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionSseEventInteraction+Status.swift new file mode 100644 index 00000000000..8cb3cfe398c --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionSseEventInteraction+Status.swift @@ -0,0 +1,70 @@ +// 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. + +extension InteractionSseEventInteraction { + /// Required. Output only. The status of the interaction. + package enum Status: Codable, Sendable, Equatable, Hashable { + + /// The interaction is in progress. + case inProgress + + /// The interaction requires action/input from the user. + case requiresAction + + /// The interaction is completed. + case completed + + /// The interaction failed. + case failed + + /// The interaction was cancelled. + case cancelled + + /// The interaction is completed, but contains incomplete results (e.g. hitting max_tokens). + case incomplete + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension InteractionSseEventInteraction.Status: RawRepresentable { + package var rawValue: String { + switch self { + case .inProgress: "in_progress" + case .requiresAction: "requires_action" + case .completed: "completed" + case .failed: "failed" + case .cancelled: "cancelled" + case .incomplete: "incomplete" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "in_progress": self = .inProgress + case "requires_action": self = .requiresAction + case "completed": self = .completed + case "failed": self = .failed + case "cancelled": self = .cancelled + case "incomplete": self = .incomplete + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionSseEventInteraction.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionSseEventInteraction.swift new file mode 100644 index 00000000000..4e049ba54d0 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionSseEventInteraction.swift @@ -0,0 +1,98 @@ +// 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. + +/// Partial interaction resource emitted by interaction lifecycle SSE events. +/// Streaming lifecycle payloads may omit fields that are only available on +/// full non-streaming Interaction responses. +package struct InteractionSseEventInteraction: Codable, Sendable, Equatable, Hashable { + + /// The agent to interact with. + package let agent: String? + + /// Output only. The time at which the response was created in ISO 8601 format. + package let created: String? + + /// Required. Output only. A unique identifier for the interaction completion. + package let id: String? + + /// The model that will complete your prompt. + package let model: String? + + /// Output only. The resource type. + package let object: String? + + /// The service tier for the interaction. + package let serviceTier: ServiceTier? + + /// Required. Output only. The status of the interaction. + package let status: Status? + + /// Output only. The steps that make up the interaction, if included in this event. + package let steps: [Step]? + + /// Output only. The time at which the response was last updated in ISO 8601 format. + package let updated: String? + + /// Output only. Statistics on the interaction request's token usage. + package let usage: Usage? + + /// Creates a new `InteractionSseEventInteraction`. + /// + /// - Parameters: + /// - agent: The agent to interact with. + /// - created: Output only. The time at which the response was created in ISO 8601 format. + /// - id: Required. Output only. A unique identifier for the interaction completion. + /// - model: The model that will complete your prompt. + /// - object: Output only. The resource type. + /// - serviceTier: The service tier for the interaction. + /// - status: Required. Output only. The status of the interaction. + /// - steps: Output only. The steps that make up the interaction, if included in this event. + /// - updated: Output only. The time at which the response was last updated in ISO 8601 format. + /// - usage: Output only. Statistics on the interaction request's token usage. + package init( + agent: String? = nil, + created: String? = nil, + id: String?, + model: String? = nil, + object: String? = nil, + serviceTier: ServiceTier? = nil, + status: Status?, + steps: [Step]? = nil, + updated: String? = nil, + usage: Usage? = nil + ) { + self.agent = agent + self.created = created + self.id = id + self.model = model + self.object = object + self.serviceTier = serviceTier + self.status = status + self.steps = steps + self.updated = updated + self.usage = usage + } + enum CodingKeys: String, CodingKey { + case agent = "agent" + case created = "created" + case id = "id" + case model = "model" + case object = "object" + case serviceTier = "service_tier" + case status = "status" + case steps = "steps" + case updated = "updated" + case usage = "usage" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionStatusUpdate+Status.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionStatusUpdate+Status.swift new file mode 100644 index 00000000000..8baabe26b3b --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionStatusUpdate+Status.swift @@ -0,0 +1,75 @@ +// 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. + +extension InteractionStatusUpdate { + package enum Status: Codable, Sendable, Equatable, Hashable { + + /// The interaction is in progress. + case inProgress + + /// The interaction requires action/input from the user. + case requiresAction + + /// The interaction is completed. + case completed + + /// The interaction failed. + case failed + + /// The interaction was cancelled. + case cancelled + + /// The interaction is completed, but contains incomplete results (e.g. + /// hitting max_tokens). + case incomplete + + /// The interaction was halted because the token budget was exceeded. + case budgetExceeded + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension InteractionStatusUpdate.Status: RawRepresentable { + package var rawValue: String { + switch self { + case .inProgress: "in_progress" + case .requiresAction: "requires_action" + case .completed: "completed" + case .failed: "failed" + case .cancelled: "cancelled" + case .incomplete: "incomplete" + case .budgetExceeded: "budget_exceeded" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "in_progress": self = .inProgress + case "requires_action": self = .requiresAction + case "completed": self = .completed + case "failed": self = .failed + case "cancelled": self = .cancelled + case "incomplete": self = .incomplete + case "budget_exceeded": self = .budgetExceeded + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionStatusUpdate.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionStatusUpdate.swift new file mode 100644 index 00000000000..433a74c8330 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionStatusUpdate.swift @@ -0,0 +1,50 @@ +// 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. + +/// An internal data model for `InteractionStatusUpdate`. +package struct InteractionStatusUpdate: Codable, Sendable, Equatable, Hashable { + + /// The event_id token to be used to resume the interaction stream, from + /// this event. + package let eventId: String? + + package let eventType: String? + + package let interactionId: String? + + package let status: Status? + + /// Creates a new `InteractionStatusUpdate`. + /// + /// - Parameters: + /// - eventId: The event_id token to be used to resume the interaction stream, from + /// - interactionId: For more details, see ``interactionId``. + /// - status: For more details, see ``status``. + package init( + eventId: String? = nil, + interactionId: String?, + status: Status? + ) { + self.eventId = eventId + self.eventType = "interaction.status_update" + self.interactionId = interactionId + self.status = status + } + enum CodingKeys: String, CodingKey { + case eventId = "event_id" + case eventType = "event_type" + case interactionId = "interaction_id" + case status = "status" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionsInput.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionsInput.swift new file mode 100644 index 00000000000..17d1543f10a --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/InteractionsInput.swift @@ -0,0 +1,65 @@ +// 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. + +package import GeminiSharedDataModels + +/// The input for the interaction. +package enum InteractionsInput: Codable, Sendable, Equatable, Hashable { + + case string(String) + case stepList([Step]) + case contentList([Content]) + case content(Content) + + package init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + if let val = try? container.decode(String.self) { + self = .string(val) + return + } + if let val = try? container.decode([Step].self) { + self = .stepList(val) + return + } + if let val = try? container.decode([Content].self) { + self = .contentList(val) + return + } + if let val = try? container.decode(Content.self) { + self = .content(val) + return + } + throw DecodingError.typeMismatch( + InteractionsInput.self, + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Failed to decode any of the variants for InteractionsInput" + ) + ) + } + + package func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .string(let val): + try container.encode(val) + case .stepList(let val): + try container.encode(val) + case .contentList(let val): + try container.encode(val) + case .content(let val): + try container.encode(val) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/MCPServer.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/MCPServer.swift new file mode 100644 index 00000000000..d18906ebbfd --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/MCPServer.swift @@ -0,0 +1,59 @@ +// 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. + +/// A MCPServer is a server that can be called by the model to perform actions. +package struct MCPServer: Codable, Sendable, Equatable, Hashable { + + /// The allowed tools. + package let allowedTools: [AllowedTools]? + + /// Optional: Fields for authentication headers, timeouts, etc., if needed. + package let headers: [String: String]? + + /// The name of the MCPServer. + package let name: String? + + package let type: String? + + /// The full URL for the MCPServer endpoint. + /// Example: "https://api.example.com/mcp" + package let url: String? + + /// Creates a new `MCPServer`. + /// + /// - Parameters: + /// - allowedTools: The allowed tools. + /// - headers: Optional: Fields for authentication headers, timeouts, etc., if needed. + /// - name: The name of the MCPServer. + /// - url: The full URL for the MCPServer endpoint. + package init( + allowedTools: [AllowedTools]? = nil, + headers: [String: String]? = nil, + name: String? = nil, + url: String? = nil + ) { + self.allowedTools = allowedTools + self.headers = headers + self.name = name + self.type = "mcp_server" + self.url = url + } + enum CodingKeys: String, CodingKey { + case allowedTools = "allowed_tools" + case headers = "headers" + case name = "name" + case type = "type" + case url = "url" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/MCPServerToolCallDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/MCPServerToolCallDelta.swift new file mode 100644 index 00000000000..c6ac4f4e3a2 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/MCPServerToolCallDelta.swift @@ -0,0 +1,50 @@ +// 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. + +package import GeminiSharedDataModels + +/// An internal data model for `MCPServerToolCallDelta`. +package struct MCPServerToolCallDelta: Codable, Sendable, Equatable, Hashable { + + package let arguments: [String: JSONValue]? + + package let name: String? + + package let serverName: String? + + package let type: String? + + /// Creates a new `MCPServerToolCallDelta`. + /// + /// - Parameters: + /// - arguments: For more details, see ``arguments``. + /// - name: For more details, see ``name``. + /// - serverName: For more details, see ``serverName``. + package init( + arguments: [String: JSONValue]?, + name: String?, + serverName: String? + ) { + self.arguments = arguments + self.name = name + self.serverName = serverName + self.type = "mcp_server_tool_call" + } + enum CodingKeys: String, CodingKey { + case arguments = "arguments" + case name = "name" + case serverName = "server_name" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/MCPServerToolCallStep.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/MCPServerToolCallStep.swift new file mode 100644 index 00000000000..6f9e5818b05 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/MCPServerToolCallStep.swift @@ -0,0 +1,60 @@ +// 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. + +package import GeminiSharedDataModels + +/// MCPServer tool call step. +package struct MCPServerToolCallStep: Codable, Sendable, Equatable, Hashable { + + /// Required. The JSON object of arguments for the function. + package let arguments: [String: JSONValue]? + + /// Required. A unique ID for this specific tool call. + package let id: String? + + /// Required. The name of the tool which was called. + package let name: String? + + /// Required. The name of the used MCP server. + package let serverName: String? + + package let type: String? + + /// Creates a new `MCPServerToolCallStep`. + /// + /// - Parameters: + /// - arguments: Required. The JSON object of arguments for the function. + /// - id: Required. A unique ID for this specific tool call. + /// - name: Required. The name of the tool which was called. + /// - serverName: Required. The name of the used MCP server. + package init( + arguments: [String: JSONValue]?, + id: String?, + name: String?, + serverName: String? + ) { + self.arguments = arguments + self.id = id + self.name = name + self.serverName = serverName + self.type = "mcp_server_tool_call" + } + enum CodingKeys: String, CodingKey { + case arguments = "arguments" + case id = "id" + case name = "name" + case serverName = "server_name" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/MCPServerToolResultDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/MCPServerToolResultDelta.swift new file mode 100644 index 00000000000..a9c7bc3e442 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/MCPServerToolResultDelta.swift @@ -0,0 +1,50 @@ +// 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. + +package import GeminiSharedDataModels + +/// An internal data model for `MCPServerToolResultDelta`. +package struct MCPServerToolResultDelta: Codable, Sendable, Equatable, Hashable { + + package let name: String? + + package let result: JSONValue? + + package let serverName: String? + + package let type: String? + + /// Creates a new `MCPServerToolResultDelta`. + /// + /// - Parameters: + /// - name: For more details, see ``name``. + /// - result: For more details, see ``result``. + /// - serverName: For more details, see ``serverName``. + package init( + name: String? = nil, + result: JSONValue?, + serverName: String? = nil + ) { + self.name = name + self.result = result + self.serverName = serverName + self.type = "mcp_server_tool_result" + } + enum CodingKeys: String, CodingKey { + case name = "name" + case result = "result" + case serverName = "server_name" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/MCPServerToolResultStep.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/MCPServerToolResultStep.swift new file mode 100644 index 00000000000..dfcf20ea916 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/MCPServerToolResultStep.swift @@ -0,0 +1,60 @@ +// 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. + +package import GeminiSharedDataModels + +/// MCPServer tool result step. +package struct MCPServerToolResultStep: Codable, Sendable, Equatable, Hashable { + + /// Required. ID to match the ID from the function call block. + package let callId: String? + + /// Name of the tool which is called for this specific tool call. + package let name: String? + + /// Required. The output from the MCP server call. Can be simple text or rich content. + package let result: JSONValue? + + /// The name of the used MCP server. + package let serverName: String? + + package let type: String? + + /// Creates a new `MCPServerToolResultStep`. + /// + /// - Parameters: + /// - callId: Required. ID to match the ID from the function call block. + /// - name: Name of the tool which is called for this specific tool call. + /// - result: Required. The output from the MCP server call. Can be simple text or rich content. + /// - serverName: The name of the used MCP server. + package init( + callId: String?, + name: String? = nil, + result: JSONValue?, + serverName: String? = nil + ) { + self.callId = callId + self.name = name + self.result = result + self.serverName = serverName + self.type = "mcp_server_tool_result" + } + enum CodingKeys: String, CodingKey { + case callId = "call_id" + case name = "name" + case result = "result" + case serverName = "server_name" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/MediaProcessing.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/MediaProcessing.swift new file mode 100644 index 00000000000..4e4b8a069bb --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/MediaProcessing.swift @@ -0,0 +1,22 @@ +// 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. + +/// An internal data model for `MediaProcessing`. +package struct MediaProcessing: Codable, Sendable, Equatable, Hashable { + + /// Creates a new `MediaProcessing`. + /// + package init() { + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/MediaResolution.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/MediaResolution.swift new file mode 100644 index 00000000000..bef816e85c6 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/MediaResolution.swift @@ -0,0 +1,58 @@ +// 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. + +/// An internal data model for `MediaResolution`. +package enum MediaResolution: Codable, Sendable, Equatable, Hashable { + + /// Low resolution. + case low + + /// Medium resolution. + case medium + + /// High resolution. + case high + + /// Ultra high resolution. + case ultraHigh + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) +} + +// MARK: - RawRepresentable Conformance + +extension MediaResolution: RawRepresentable { + package var rawValue: String { + switch self { + case .low: "low" + case .medium: "medium" + case .high: "high" + case .ultraHigh: "ultra_high" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "low": self = .low + case "medium": self = .medium + case "high": self = .high + case "ultra_high": self = .ultraHigh + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ModalityTokens.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ModalityTokens.swift new file mode 100644 index 00000000000..5a883af6fae --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ModalityTokens.swift @@ -0,0 +1,40 @@ +// 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. + +/// The token count for a single response modality. +package struct ModalityTokens: Codable, Sendable, Equatable, Hashable { + + /// The modality associated with the token count. + package let modality: ResponseModality? + + /// Number of tokens for the modality. + package let tokens: Int? + + /// Creates a new `ModalityTokens`. + /// + /// - Parameters: + /// - modality: The modality associated with the token count. + /// - tokens: Number of tokens for the modality. + package init( + modality: ResponseModality? = nil, + tokens: Int? = nil + ) { + self.modality = modality + self.tokens = tokens + } + enum CodingKeys: String, CodingKey { + case modality = "modality" + case tokens = "tokens" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/Model.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/Model.swift new file mode 100644 index 00000000000..a1b655682ae --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/Model.swift @@ -0,0 +1,153 @@ +// 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. + +/// The model that will complete your prompt.\n\nSee [models](https://ai.google.dev/gemini-api/docs/models) for additional details. +package enum Model: Codable, Sendable, Equatable, Hashable { + + /// Our first hybrid reasoning model which supports a 1M token context window and has thinking budgets. + case gemini25Flash + + /// Our state-of-the-art multipurpose model, which excels at coding and complex reasoning tasks. + case gemini25Pro + + /// Gemma 4 26B A4B IT + case gemma426bA4bIt + + /// Gemma 4 31B IT + case gemma431bIt + + /// Latest release of Gemini Flash + case geminiFlashLatest + + /// Latest release of Gemini Flash-Lite + case geminiFlashLiteLatest + + /// Latest release of Gemini Pro + case geminiProLatest + + /// Our smallest and most cost effective model, built for at scale usage. + case gemini25FlashLite + + /// Our native image generation model, optimized for speed, flexibility, and contextual understanding. Text input and output is priced the same as 2.5 Flash. + case gemini25FlashImage + + /// Our most intelligent model built for speed, combining frontier intelligence with superior search and grounding. + case gemini3FlashPreview + + /// Our latest SOTA reasoning model with unprecedented depth and nuance, and powerful multimodal understanding and coding capabilities. + case gemini31ProPreview + + /// Gemini 3.1 Pro Preview optimized for custom tool usage + case gemini31ProPreviewCustomtools + + /// Our most cost-efficient model, optimized for high-volume agentic tasks, translation, and simple data processing. + case gemini31FlashLite + + /// Gemini 3 Pro Image + case gemini3ProImage + + /// Gemini 3 Pro Image Preview + case nanoBananaProPreview + + /// Gemini 3.1 Flash Image. + case gemini31FlashImage + + /// Our most intelligent model for sustained frontier performance in agentic and coding tasks. + case gemini35Flash + + /// Our most intelligent model for sustained frontier performance in agentic and coding tasks. + case gemini36Flash + + /// Our most intelligent model for sustained frontier performance in agentic and coding tasks. + case gemini37Flash + + /// Our low-latency, music generation model optimized for high-fidelity audio clips and precise rhythmic control. + case lyria3ClipPreview + + /// Our advanced, full-song generative model with deep compositional understanding, optimized for precise structural control and complex transitions across diverse musical styles. + case lyria3ProPreview + + /// Gemini Robotics-ER 1.6 Preview + case geminiRoboticsEr16Preview + + /// Gemini Robotics Embodied Reasoning 2 Preview + case geminiRoboticsEr2Preview + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) +} + +// MARK: - RawRepresentable Conformance + +extension Model: RawRepresentable { + package var rawValue: String { + switch self { + case .gemini25Flash: "gemini-2.5-flash" + case .gemini25Pro: "gemini-2.5-pro" + case .gemma426bA4bIt: "gemma-4-26b-a4b-it" + case .gemma431bIt: "gemma-4-31b-it" + case .geminiFlashLatest: "gemini-flash-latest" + case .geminiFlashLiteLatest: "gemini-flash-lite-latest" + case .geminiProLatest: "gemini-pro-latest" + case .gemini25FlashLite: "gemini-2.5-flash-lite" + case .gemini25FlashImage: "gemini-2.5-flash-image" + case .gemini3FlashPreview: "gemini-3-flash-preview" + case .gemini31ProPreview: "gemini-3.1-pro-preview" + case .gemini31ProPreviewCustomtools: "gemini-3.1-pro-preview-customtools" + case .gemini31FlashLite: "gemini-3.1-flash-lite" + case .gemini3ProImage: "gemini-3-pro-image" + case .nanoBananaProPreview: "nano-banana-pro-preview" + case .gemini31FlashImage: "gemini-3.1-flash-image" + case .gemini35Flash: "gemini-3.5-flash" + case .gemini36Flash: "gemini-3.6-flash" + case .gemini37Flash: "gemini-3.7-flash" + case .lyria3ClipPreview: "lyria-3-clip-preview" + case .lyria3ProPreview: "lyria-3-pro-preview" + case .geminiRoboticsEr16Preview: "gemini-robotics-er-1.6-preview" + case .geminiRoboticsEr2Preview: "gemini-robotics-er-2-preview" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "gemini-2.5-flash": self = .gemini25Flash + case "gemini-2.5-pro": self = .gemini25Pro + case "gemma-4-26b-a4b-it": self = .gemma426bA4bIt + case "gemma-4-31b-it": self = .gemma431bIt + case "gemini-flash-latest": self = .geminiFlashLatest + case "gemini-flash-lite-latest": self = .geminiFlashLiteLatest + case "gemini-pro-latest": self = .geminiProLatest + case "gemini-2.5-flash-lite": self = .gemini25FlashLite + case "gemini-2.5-flash-image": self = .gemini25FlashImage + case "gemini-3-flash-preview": self = .gemini3FlashPreview + case "gemini-3.1-pro-preview": self = .gemini31ProPreview + case "gemini-3.1-pro-preview-customtools": self = .gemini31ProPreviewCustomtools + case "gemini-3.1-flash-lite": self = .gemini31FlashLite + case "gemini-3-pro-image": self = .gemini3ProImage + case "nano-banana-pro-preview": self = .nanoBananaProPreview + case "gemini-3.1-flash-image": self = .gemini31FlashImage + case "gemini-3.5-flash": self = .gemini35Flash + case "gemini-3.6-flash": self = .gemini36Flash + case "gemini-3.7-flash": self = .gemini37Flash + case "lyria-3-clip-preview": self = .lyria3ClipPreview + case "lyria-3-pro-preview": self = .lyria3ProPreview + case "gemini-robotics-er-1.6-preview": self = .geminiRoboticsEr16Preview + case "gemini-robotics-er-2-preview": self = .geminiRoboticsEr2Preview + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ModelOutputStep.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ModelOutputStep.swift new file mode 100644 index 00000000000..ad5ec45e576 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ModelOutputStep.swift @@ -0,0 +1,36 @@ +// 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. + +/// Output generated by the model. +package struct ModelOutputStep: Codable, Sendable, Equatable, Hashable { + + package let content: [Content]? + + package let type: String? + + /// Creates a new `ModelOutputStep`. + /// + /// - Parameters: + /// - content: For more details, see ``content``. + package init( + content: [Content]? = nil + ) { + self.content = content + self.type = "model_output" + } + enum CodingKeys: String, CodingKey { + case content = "content" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/PlaceCitation.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/PlaceCitation.swift new file mode 100644 index 00000000000..4ed3c5f233b --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/PlaceCitation.swift @@ -0,0 +1,75 @@ +// 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. + +/// A place citation annotation. +package struct PlaceCitation: Codable, Sendable, Equatable, Hashable { + + /// End of the attributed segment, exclusive. + package let endIndex: Int? + + /// Title of the place. + package let name: String? + + /// The ID of the place, in `places/{place_id}` format. + package let placeId: String? + + /// Snippets of reviews that are used to generate answers about the + /// features of a given place in Google Maps. + package let reviewSnippets: [ReviewSnippet]? + + /// Start of segment of the response that is attributed to this source. + /// + /// Index indicates the start of the segment, measured in bytes. + package let startIndex: Int? + + package let type: String? + + /// URI reference of the place. + package let url: String? + + /// Creates a new `PlaceCitation`. + /// + /// - Parameters: + /// - endIndex: End of the attributed segment, exclusive. + /// - name: Title of the place. + /// - placeId: The ID of the place, in `places/{place_id}` format. + /// - reviewSnippets: Snippets of reviews that are used to generate answers about the + /// - startIndex: Start of segment of the response that is attributed to this source. + /// - url: URI reference of the place. + package init( + endIndex: Int? = nil, + name: String? = nil, + placeId: String? = nil, + reviewSnippets: [ReviewSnippet]? = nil, + startIndex: Int? = nil, + url: String? = nil + ) { + self.endIndex = endIndex + self.name = name + self.placeId = placeId + self.reviewSnippets = reviewSnippets + self.startIndex = startIndex + self.type = "place_citation" + self.url = url + } + enum CodingKeys: String, CodingKey { + case endIndex = "end_index" + case name = "name" + case placeId = "place_id" + case reviewSnippets = "review_snippets" + case startIndex = "start_index" + case type = "type" + case url = "url" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ProcessingCallDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ProcessingCallDelta.swift new file mode 100644 index 00000000000..ae3d2234c16 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ProcessingCallDelta.swift @@ -0,0 +1,39 @@ +// 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. + +package import Foundation + +/// Streaming delta for a server-initiated media processing step. +package struct ProcessingCallDelta: Codable, Sendable, Equatable, Hashable { + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `ProcessingCallDelta`. + /// + /// - Parameters: + /// - signature: A signature hash for backend validation. + package init( + signature: Data? = nil + ) { + self.signature = signature + self.type = "processing_call" + } + enum CodingKeys: String, CodingKey { + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ProcessingCallStep.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ProcessingCallStep.swift new file mode 100644 index 00000000000..f7d7caadaca --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ProcessingCallStep.swift @@ -0,0 +1,47 @@ +// 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. + +package import Foundation + +/// A server-initiated processing step for media analysis (e.g. video +/// understanding). +package struct ProcessingCallStep: Codable, Sendable, Equatable, Hashable { + + /// Required. A unique ID for this specific tool call. + package let id: String? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `ProcessingCallStep`. + /// + /// - Parameters: + /// - id: Required. A unique ID for this specific tool call. + /// - signature: A signature hash for backend validation. + package init( + id: String?, + signature: Data? = nil + ) { + self.id = id + self.signature = signature + self.type = "processing_call" + } + enum CodingKeys: String, CodingKey { + case id = "id" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ProcessingResultDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ProcessingResultDelta.swift new file mode 100644 index 00000000000..f1f70606928 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ProcessingResultDelta.swift @@ -0,0 +1,39 @@ +// 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. + +package import Foundation + +/// Streaming delta for the result of a server-initiated media processing step. +package struct ProcessingResultDelta: Codable, Sendable, Equatable, Hashable { + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `ProcessingResultDelta`. + /// + /// - Parameters: + /// - signature: A signature hash for backend validation. + package init( + signature: Data? = nil + ) { + self.signature = signature + self.type = "processing_result" + } + enum CodingKeys: String, CodingKey { + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ProcessingResultStep.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ProcessingResultStep.swift new file mode 100644 index 00000000000..f1a38ad1815 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ProcessingResultStep.swift @@ -0,0 +1,46 @@ +// 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. + +package import Foundation + +/// The result of a server-initiated media processing step. +package struct ProcessingResultStep: Codable, Sendable, Equatable, Hashable { + + /// Required. ID to match the ID from the function call block. + package let callId: String? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `ProcessingResultStep`. + /// + /// - Parameters: + /// - callId: Required. ID to match the ID from the function call block. + /// - signature: A signature hash for backend validation. + package init( + callId: String?, + signature: Data? = nil + ) { + self.callId = callId + self.signature = signature + self.type = "processing_result" + } + enum CodingKeys: String, CodingKey { + case callId = "call_id" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ResponseFormat.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ResponseFormat.swift new file mode 100644 index 00000000000..16c07a1019c --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ResponseFormat.swift @@ -0,0 +1,72 @@ +// 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. + +package import GeminiSharedDataModels + +/// An internal data model for `ResponseFormat`. +package enum ResponseFormat: Codable, Sendable, Equatable, Hashable { + + case audioResponseFormat(AudioResponseFormat) + case textResponseFormat(TextResponseFormat) + case imageResponseFormat(ImageResponseFormat) + case videoResponseFormat(VideoResponseFormat) + case custom([String: JSONValue]) + + package init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + if let val = try? container.decode(AudioResponseFormat.self) { + self = .audioResponseFormat(val) + return + } + if let val = try? container.decode(TextResponseFormat.self) { + self = .textResponseFormat(val) + return + } + if let val = try? container.decode(ImageResponseFormat.self) { + self = .imageResponseFormat(val) + return + } + if let val = try? container.decode(VideoResponseFormat.self) { + self = .videoResponseFormat(val) + return + } + if let val = try? container.decode([String: JSONValue].self) { + self = .custom(val) + return + } + throw DecodingError.typeMismatch( + ResponseFormat.self, + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Failed to decode any of the variants for ResponseFormat" + ) + ) + } + + package func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .audioResponseFormat(let val): + try container.encode(val) + case .textResponseFormat(let val): + try container.encode(val) + case .imageResponseFormat(let val): + try container.encode(val) + case .videoResponseFormat(let val): + try container.encode(val) + case .custom(let val): + try container.encode(val) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ResponseModality.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ResponseModality.swift new file mode 100644 index 00000000000..5e42d0bd75d --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ResponseModality.swift @@ -0,0 +1,63 @@ +// 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. + +/// An internal data model for `ResponseModality`. +package enum ResponseModality: Codable, Sendable, Equatable, Hashable { + + /// Indicates the model should return text. + case text + + /// Indicates the model should return images. + case image + + /// Indicates the model should return audio. + case audio + + /// Indicates the model should return video. + case video + + /// Indicates the model should return documents. + case document + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) +} + +// MARK: - RawRepresentable Conformance + +extension ResponseModality: RawRepresentable { + package var rawValue: String { + switch self { + case .text: "text" + case .image: "image" + case .audio: "audio" + case .video: "video" + case .document: "document" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "text": self = .text + case "image": self = .image + case "audio": self = .audio + case "video": self = .video + case "document": self = .document + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ReviewSnippet.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ReviewSnippet.swift new file mode 100644 index 00000000000..65a142a942a --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ReviewSnippet.swift @@ -0,0 +1,48 @@ +// 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. + +/// Encapsulates a snippet of a user review that answers a question about +/// the features of a specific place in Google Maps. +package struct ReviewSnippet: Codable, Sendable, Equatable, Hashable { + + /// The ID of the review snippet. + package let reviewId: String? + + /// Title of the review. + package let title: String? + + /// A link that corresponds to the user review on Google Maps. + package let url: String? + + /// Creates a new `ReviewSnippet`. + /// + /// - Parameters: + /// - reviewId: The ID of the review snippet. + /// - title: Title of the review. + /// - url: A link that corresponds to the user review on Google Maps. + package init( + reviewId: String? = nil, + title: String? = nil, + url: String? = nil + ) { + self.reviewId = reviewId + self.title = title + self.url = url + } + enum CodingKeys: String, CodingKey { + case reviewId = "review_id" + case title = "title" + case url = "url" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/SafetySetting+Method.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/SafetySetting+Method.swift new file mode 100644 index 00000000000..1b2393a87be --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/SafetySetting+Method.swift @@ -0,0 +1,51 @@ +// 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. + +extension SafetySetting { + /// Optional. The method for blocking content. If not specified, the default + /// behavior is to use the probability score. + package enum Method: Codable, Sendable, Equatable, Hashable { + + /// The harm block method uses both probability and severity scores. + case severity + + /// The harm block method uses the probability score. + case probability + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension SafetySetting.Method: RawRepresentable { + package var rawValue: String { + switch self { + case .severity: "severity" + case .probability: "probability" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "severity": self = .severity + case "probability": self = .probability + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/SafetySetting+Threshold.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/SafetySetting+Threshold.swift new file mode 100644 index 00000000000..52dc3baec66 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/SafetySetting+Threshold.swift @@ -0,0 +1,66 @@ +// 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. + +extension SafetySetting { + /// Required. The threshold for blocking content. If the harm probability + /// exceeds this threshold, the content will be blocked. + package enum Threshold: Codable, Sendable, Equatable, Hashable { + + /// Block content with a low harm probability or higher. + case blockLowAndAbove + + /// Block content with a medium harm probability or higher. + case blockMediumAndAbove + + /// Block content with a high harm probability. + case blockOnlyHigh + + /// Do not block any content, regardless of its harm probability. + case blockNone + + /// Turn off the safety filter entirely. + case off + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension SafetySetting.Threshold: RawRepresentable { + package var rawValue: String { + switch self { + case .blockLowAndAbove: "block_low_and_above" + case .blockMediumAndAbove: "block_medium_and_above" + case .blockOnlyHigh: "block_only_high" + case .blockNone: "block_none" + case .off: "off" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "block_low_and_above": self = .blockLowAndAbove + case "block_medium_and_above": self = .blockMediumAndAbove + case "block_only_high": self = .blockOnlyHigh + case "block_none": self = .blockNone + case "off": self = .off + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/SafetySetting.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/SafetySetting.swift new file mode 100644 index 00000000000..59779414110 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/SafetySetting.swift @@ -0,0 +1,54 @@ +// 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. + +/// A safety setting that affects the safety-blocking behavior. +/// +/// A SafetySetting consists of a +/// harm category and a +/// threshold for that +/// category. +package struct SafetySetting: Codable, Sendable, Equatable, Hashable { + + /// Optional. The method for blocking content. If not specified, the default + /// behavior is to use the probability score. + package let method: Method? + + /// Required. The threshold for blocking content. If the harm probability + /// exceeds this threshold, the content will be blocked. + package let threshold: Threshold? + + /// Required. The type of harm category to be blocked. + package let type: HarmCategory? + + /// Creates a new `SafetySetting`. + /// + /// - Parameters: + /// - method: Optional. The method for blocking content. If not specified, the default + /// - threshold: Required. The threshold for blocking content. If the harm probability + /// - type: Required. The type of harm category to be blocked. + package init( + method: Method? = nil, + threshold: Threshold?, + type: HarmCategory? + ) { + self.method = method + self.threshold = threshold + self.type = type + } + enum CodingKeys: String, CodingKey { + case method = "method" + case threshold = "threshold" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ServiceTier.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ServiceTier.swift new file mode 100644 index 00000000000..6208e3df3e9 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ServiceTier.swift @@ -0,0 +1,53 @@ +// 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. + +/// An internal data model for `ServiceTier`. +package enum ServiceTier: Codable, Sendable, Equatable, Hashable { + + /// Flex service tier. + case flex + + /// Standard service tier. + case standard + + /// Priority service tier. + case priority + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) +} + +// MARK: - RawRepresentable Conformance + +extension ServiceTier: RawRepresentable { + package var rawValue: String { + switch self { + case .flex: "flex" + case .standard: "standard" + case .priority: "priority" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "flex": self = .flex + case "standard": self = .standard + case "priority": self = .priority + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/SmartTranscriptionMode.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/SmartTranscriptionMode.swift new file mode 100644 index 00000000000..7117cfaa3a0 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/SmartTranscriptionMode.swift @@ -0,0 +1,28 @@ +// 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. + +/// Configuration for smart transcription mode. +package struct SmartTranscriptionMode: Codable, Sendable, Equatable, Hashable { + + package let type: String? + + /// Creates a new `SmartTranscriptionMode`. + /// + package init() { + self.type = "smart" + } + enum CodingKeys: String, CodingKey { + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/Source+Type.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/Source+Type.swift new file mode 100644 index 00000000000..67f6c98bc65 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/Source+Type.swift @@ -0,0 +1,55 @@ +// 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. + +extension Source { + package enum `Type`: Codable, Sendable, Equatable, Hashable { + + /// A Cloud Storage bucket. + case gcs + + /// Inline content. + case inline + + /// A generic repository. The protocol prefix in the source URL + /// identifies the provider (e.g., github://, gcs://). + case repository + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension Source.`Type`: RawRepresentable { + package var rawValue: String { + switch self { + case .gcs: "gcs" + case .inline: "inline" + case .repository: "repository" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "gcs": self = .gcs + case "inline": self = .inline + case "repository": self = .repository + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/Source.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/Source.swift new file mode 100644 index 00000000000..a221c0f94bc --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/Source.swift @@ -0,0 +1,62 @@ +// 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. + +/// A source to be mounted into the environment. +package struct Source: Codable, Sendable, Equatable, Hashable { + + /// The inline content if `type` is `INLINE`. + package let content: String? + + /// Optional encoding for inline content (e.g. `base64`). + package let encoding: String? + + /// The source of the environment. + /// For Cloud Storage, this is the Cloud Storage path. + /// For GitHub, this is the GitHub path. + package let source: String? + + /// Where the source should appear in the environment. + package let target: String? + + package let type: `Type`? + + /// Creates a new `Source`. + /// + /// - Parameters: + /// - content: The inline content if `type` is `INLINE`. + /// - encoding: Optional encoding for inline content (e.g. `base64`). + /// - source: The source of the environment. + /// - target: Where the source should appear in the environment. + /// - type: For more details, see ``type``. + package init( + content: String? = nil, + encoding: String? = nil, + source: String? = nil, + target: String? = nil, + type: `Type`? = nil + ) { + self.content = content + self.encoding = encoding + self.source = source + self.target = target + self.type = type + } + enum CodingKeys: String, CodingKey { + case content = "content" + case encoding = "encoding" + case source = "source" + case target = "target" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/SpeakerConfig.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/SpeakerConfig.swift new file mode 100644 index 00000000000..7093a3afcc3 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/SpeakerConfig.swift @@ -0,0 +1,33 @@ +// 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. + +/// Configuration for multi-speaker and speech generation. +package struct SpeakerConfig: Codable, Sendable, Equatable, Hashable { + + /// Individual speaker configurations. + package let speakers: [SpeechConfig]? + + /// Creates a new `SpeakerConfig`. + /// + /// - Parameters: + /// - speakers: Individual speaker configurations. + package init( + speakers: [SpeechConfig]? = nil + ) { + self.speakers = speakers + } + enum CodingKeys: String, CodingKey { + case speakers = "speakers" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/SpeechConfig.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/SpeechConfig.swift new file mode 100644 index 00000000000..4abfbd6e6cd --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/SpeechConfig.swift @@ -0,0 +1,47 @@ +// 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. + +/// The configuration for speech interaction. +package struct SpeechConfig: Codable, Sendable, Equatable, Hashable { + + /// The language of the speech. + package let language: String? + + /// The speaker's name, it should match the speaker name given in the prompt. + package let speaker: String? + + /// The voice of the speaker. + package let voice: String? + + /// Creates a new `SpeechConfig`. + /// + /// - Parameters: + /// - language: The language of the speech. + /// - speaker: The speaker's name, it should match the speaker name given in the prompt. + /// - voice: The voice of the speaker. + package init( + language: String? = nil, + speaker: String? = nil, + voice: String? = nil + ) { + self.language = language + self.speaker = speaker + self.voice = voice + } + enum CodingKeys: String, CodingKey { + case language = "language" + case speaker = "speaker" + case voice = "voice" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/StaticMediaProcessing.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/StaticMediaProcessing.swift new file mode 100644 index 00000000000..4856a414966 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/StaticMediaProcessing.swift @@ -0,0 +1,54 @@ +// 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. + +/// An internal data model for `StaticMediaProcessing`. +package struct StaticMediaProcessing: Codable, Sendable, Equatable, Hashable { + + /// Optional. Segment end time. Specified as a decimal number of seconds followed + /// by an 's' suffix, e.g., "30s". Must be non-negative and greater than + /// `start_offset` if `start_offset` is set. + package let endOffset: String? + + /// Optional. Video frame-rate sampling density. + package let fps: Double? + + /// Optional. Segment start time. Specified as a decimal number of seconds followed + /// by an 's' suffix, e.g., "10.5s". Must be non-negative. + package let startOffset: String? + + package let type: String? + + /// Creates a new `StaticMediaProcessing`. + /// + /// - Parameters: + /// - endOffset: Optional. Segment end time. Specified as a decimal number of seconds followed + /// - fps: Optional. Video frame-rate sampling density. + /// - startOffset: Optional. Segment start time. Specified as a decimal number of seconds followed + package init( + endOffset: String? = nil, + fps: Double? = nil, + startOffset: String? = nil + ) { + self.endOffset = endOffset + self.fps = fps + self.startOffset = startOffset + self.type = "static" + } + enum CodingKeys: String, CodingKey { + case endOffset = "end_offset" + case fps = "fps" + case startOffset = "start_offset" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/Step.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/Step.swift new file mode 100644 index 00000000000..54c981f788c --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/Step.swift @@ -0,0 +1,196 @@ +// 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. + +package import GeminiSharedDataModels + +/// A step in the interaction. +package enum Step: Codable, Sendable, Equatable, Hashable { + + /// Code execution call step. + case codeExecutionCallStep(CodeExecutionCallStep) + + /// Code execution result step. + case codeExecutionResultStep(CodeExecutionResultStep) + + /// File Search call step. + case fileSearchCallStep(FileSearchCallStep) + + /// File Search result step. + case fileSearchResultStep(FileSearchResultStep) + + /// A function tool call step. + case functionCallStep(FunctionCallStep) + + /// Result of a function tool call. + case functionResultStep(FunctionResultStep) + + /// Google Maps call step. + case googleMapsCallStep(GoogleMapsCallStep) + + /// Google Maps result step. + case googleMapsResultStep(GoogleMapsResultStep) + + /// Google Search call step. + case googleSearchCallStep(GoogleSearchCallStep) + + /// Google Search result step. + case googleSearchResultStep(GoogleSearchResultStep) + + /// MCPServer tool call step. + case mCPServerToolCallStep(MCPServerToolCallStep) + + /// MCPServer tool result step. + case mCPServerToolResultStep(MCPServerToolResultStep) + + /// Output generated by the model. + case modelOutputStep(ModelOutputStep) + + /// A server-initiated processing step for media analysis (e.g. video + /// understanding). + case processingCallStep(ProcessingCallStep) + + /// The result of a server-initiated media processing step. + case processingResultStep(ProcessingResultStep) + + /// A thought step. + case thoughtStep(ThoughtStep) + + /// URL context call step. + case uRLContextCallStep(URLContextCallStep) + + /// URL context result step. + case uRLContextResultStep(URLContextResultStep) + + /// Input provided by the user. + case userInputStep(UserInputStep) + + /// Unrecognized case. + case unrecognized(JSONValue) + + private enum CodingKeys: String, CodingKey { + case discriminator = "type" + } + + package init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let discValue = try container.decode(String.self, forKey: .discriminator) + switch discValue { + case "code_execution_call": + let val = try CodeExecutionCallStep(from: decoder) + self = .codeExecutionCallStep(val) + case "code_execution_result": + let val = try CodeExecutionResultStep(from: decoder) + self = .codeExecutionResultStep(val) + case "file_search_call": + let val = try FileSearchCallStep(from: decoder) + self = .fileSearchCallStep(val) + case "file_search_result": + let val = try FileSearchResultStep(from: decoder) + self = .fileSearchResultStep(val) + case "function_call": + let val = try FunctionCallStep(from: decoder) + self = .functionCallStep(val) + case "function_result": + let val = try FunctionResultStep(from: decoder) + self = .functionResultStep(val) + case "google_maps_call": + let val = try GoogleMapsCallStep(from: decoder) + self = .googleMapsCallStep(val) + case "google_maps_result": + let val = try GoogleMapsResultStep(from: decoder) + self = .googleMapsResultStep(val) + case "google_search_call": + let val = try GoogleSearchCallStep(from: decoder) + self = .googleSearchCallStep(val) + case "google_search_result": + let val = try GoogleSearchResultStep(from: decoder) + self = .googleSearchResultStep(val) + case "mcp_server_tool_call": + let val = try MCPServerToolCallStep(from: decoder) + self = .mCPServerToolCallStep(val) + case "mcp_server_tool_result": + let val = try MCPServerToolResultStep(from: decoder) + self = .mCPServerToolResultStep(val) + case "model_output": + let val = try ModelOutputStep(from: decoder) + self = .modelOutputStep(val) + case "processing_call": + let val = try ProcessingCallStep(from: decoder) + self = .processingCallStep(val) + case "processing_result": + let val = try ProcessingResultStep(from: decoder) + self = .processingResultStep(val) + case "thought": + let val = try ThoughtStep(from: decoder) + self = .thoughtStep(val) + case "url_context_call": + let val = try URLContextCallStep(from: decoder) + self = .uRLContextCallStep(val) + case "url_context_result": + let val = try URLContextResultStep(from: decoder) + self = .uRLContextResultStep(val) + case "user_input": + let val = try UserInputStep(from: decoder) + self = .userInputStep(val) + default: + let val = try JSONValue(from: decoder) + self = .unrecognized(val) + } + } + + package func encode(to encoder: any Encoder) throws { + switch self { + case .codeExecutionCallStep(let val): + try val.encode(to: encoder) + case .codeExecutionResultStep(let val): + try val.encode(to: encoder) + case .fileSearchCallStep(let val): + try val.encode(to: encoder) + case .fileSearchResultStep(let val): + try val.encode(to: encoder) + case .functionCallStep(let val): + try val.encode(to: encoder) + case .functionResultStep(let val): + try val.encode(to: encoder) + case .googleMapsCallStep(let val): + try val.encode(to: encoder) + case .googleMapsResultStep(let val): + try val.encode(to: encoder) + case .googleSearchCallStep(let val): + try val.encode(to: encoder) + case .googleSearchResultStep(let val): + try val.encode(to: encoder) + case .mCPServerToolCallStep(let val): + try val.encode(to: encoder) + case .mCPServerToolResultStep(let val): + try val.encode(to: encoder) + case .modelOutputStep(let val): + try val.encode(to: encoder) + case .processingCallStep(let val): + try val.encode(to: encoder) + case .processingResultStep(let val): + try val.encode(to: encoder) + case .thoughtStep(let val): + try val.encode(to: encoder) + case .uRLContextCallStep(let val): + try val.encode(to: encoder) + case .uRLContextResultStep(let val): + try val.encode(to: encoder) + case .userInputStep(let val): + try val.encode(to: encoder) + case .unrecognized(let val): + try val.encode(to: encoder) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/StepDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/StepDelta.swift new file mode 100644 index 00000000000..f780e6d0e7a --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/StepDelta.swift @@ -0,0 +1,56 @@ +// 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. + +/// An internal data model for `StepDelta`. +package struct StepDelta: Codable, Sendable, Equatable, Hashable { + + package let delta: StepDeltaData? + + /// The event_id token to be used to resume the interaction stream, from + /// this event. + package let eventId: String? + + package let eventType: String? + + package let index: Int? + + package let metadata: StepDeltaMetadata? + + /// Creates a new `StepDelta`. + /// + /// - Parameters: + /// - delta: For more details, see ``delta``. + /// - eventId: The event_id token to be used to resume the interaction stream, from + /// - index: For more details, see ``index``. + /// - metadata: For more details, see ``metadata``. + package init( + delta: StepDeltaData?, + eventId: String? = nil, + index: Int?, + metadata: StepDeltaMetadata? = nil + ) { + self.delta = delta + self.eventId = eventId + self.eventType = "step.delta" + self.index = index + self.metadata = metadata + } + enum CodingKeys: String, CodingKey { + case delta = "delta" + case eventId = "event_id" + case eventType = "event_type" + case index = "index" + case metadata = "metadata" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/StepDeltaData.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/StepDeltaData.swift new file mode 100644 index 00000000000..5acef9c2702 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/StepDeltaData.swift @@ -0,0 +1,235 @@ +// 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. + +package import GeminiSharedDataModels + +/// An internal data model for `StepDeltaData`. +package enum StepDeltaData: Codable, Sendable, Equatable, Hashable { + + /// An internal data model for `ArgumentsDelta`. + case argumentsDelta(ArgumentsDelta) + + /// An internal data model for `AudioDelta`. + case audioDelta(AudioDelta) + + /// An internal data model for `CodeExecutionCallDelta`. + case codeExecutionCallDelta(CodeExecutionCallDelta) + + /// An internal data model for `CodeExecutionResultDelta`. + case codeExecutionResultDelta(CodeExecutionResultDelta) + + /// An internal data model for `DocumentDelta`. + case documentDelta(DocumentDelta) + + /// An internal data model for `FileSearchCallDelta`. + case fileSearchCallDelta(FileSearchCallDelta) + + /// An internal data model for `FileSearchResultDelta`. + case fileSearchResultDelta(FileSearchResultDelta) + + /// An internal data model for `FunctionResultDelta`. + case functionResultDelta(FunctionResultDelta) + + /// An internal data model for `GoogleMapsCallDelta`. + case googleMapsCallDelta(GoogleMapsCallDelta) + + /// An internal data model for `GoogleMapsResultDelta`. + case googleMapsResultDelta(GoogleMapsResultDelta) + + /// An internal data model for `GoogleSearchCallDelta`. + case googleSearchCallDelta(GoogleSearchCallDelta) + + /// An internal data model for `GoogleSearchResultDelta`. + case googleSearchResultDelta(GoogleSearchResultDelta) + + /// An internal data model for `ImageDelta`. + case imageDelta(ImageDelta) + + /// An internal data model for `MCPServerToolCallDelta`. + case mCPServerToolCallDelta(MCPServerToolCallDelta) + + /// An internal data model for `MCPServerToolResultDelta`. + case mCPServerToolResultDelta(MCPServerToolResultDelta) + + /// Streaming delta for a server-initiated media processing step. + case processingCallDelta(ProcessingCallDelta) + + /// Streaming delta for the result of a server-initiated media processing step. + case processingResultDelta(ProcessingResultDelta) + + /// An internal data model for `TextAnnotationDelta`. + case textAnnotationDelta(TextAnnotationDelta) + + /// An internal data model for `TextDelta`. + case textDelta(TextDelta) + + /// An internal data model for `ThoughtSignatureDelta`. + case thoughtSignatureDelta(ThoughtSignatureDelta) + + /// An internal data model for `ThoughtSummaryDelta`. + case thoughtSummaryDelta(ThoughtSummaryDelta) + + /// An internal data model for `URLContextCallDelta`. + case uRLContextCallDelta(URLContextCallDelta) + + /// An internal data model for `URLContextResultDelta`. + case uRLContextResultDelta(URLContextResultDelta) + + /// An internal data model for `VideoDelta`. + case videoDelta(VideoDelta) + + /// Unrecognized case. + case unrecognized(JSONValue) + + private enum CodingKeys: String, CodingKey { + case discriminator = "type" + } + + package init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let discValue = try container.decode(String.self, forKey: .discriminator) + switch discValue { + case "arguments_delta": + let val = try ArgumentsDelta(from: decoder) + self = .argumentsDelta(val) + case "audio": + let val = try AudioDelta(from: decoder) + self = .audioDelta(val) + case "code_execution_call": + let val = try CodeExecutionCallDelta(from: decoder) + self = .codeExecutionCallDelta(val) + case "code_execution_result": + let val = try CodeExecutionResultDelta(from: decoder) + self = .codeExecutionResultDelta(val) + case "document": + let val = try DocumentDelta(from: decoder) + self = .documentDelta(val) + case "file_search_call": + let val = try FileSearchCallDelta(from: decoder) + self = .fileSearchCallDelta(val) + case "file_search_result": + let val = try FileSearchResultDelta(from: decoder) + self = .fileSearchResultDelta(val) + case "function_result": + let val = try FunctionResultDelta(from: decoder) + self = .functionResultDelta(val) + case "google_maps_call": + let val = try GoogleMapsCallDelta(from: decoder) + self = .googleMapsCallDelta(val) + case "google_maps_result": + let val = try GoogleMapsResultDelta(from: decoder) + self = .googleMapsResultDelta(val) + case "google_search_call": + let val = try GoogleSearchCallDelta(from: decoder) + self = .googleSearchCallDelta(val) + case "google_search_result": + let val = try GoogleSearchResultDelta(from: decoder) + self = .googleSearchResultDelta(val) + case "image": + let val = try ImageDelta(from: decoder) + self = .imageDelta(val) + case "mcp_server_tool_call": + let val = try MCPServerToolCallDelta(from: decoder) + self = .mCPServerToolCallDelta(val) + case "mcp_server_tool_result": + let val = try MCPServerToolResultDelta(from: decoder) + self = .mCPServerToolResultDelta(val) + case "processing_call": + let val = try ProcessingCallDelta(from: decoder) + self = .processingCallDelta(val) + case "processing_result": + let val = try ProcessingResultDelta(from: decoder) + self = .processingResultDelta(val) + case "text_annotation_delta": + let val = try TextAnnotationDelta(from: decoder) + self = .textAnnotationDelta(val) + case "text": + let val = try TextDelta(from: decoder) + self = .textDelta(val) + case "thought_signature": + let val = try ThoughtSignatureDelta(from: decoder) + self = .thoughtSignatureDelta(val) + case "thought_summary": + let val = try ThoughtSummaryDelta(from: decoder) + self = .thoughtSummaryDelta(val) + case "url_context_call": + let val = try URLContextCallDelta(from: decoder) + self = .uRLContextCallDelta(val) + case "url_context_result": + let val = try URLContextResultDelta(from: decoder) + self = .uRLContextResultDelta(val) + case "video": + let val = try VideoDelta(from: decoder) + self = .videoDelta(val) + default: + let val = try JSONValue(from: decoder) + self = .unrecognized(val) + } + } + + package func encode(to encoder: any Encoder) throws { + switch self { + case .argumentsDelta(let val): + try val.encode(to: encoder) + case .audioDelta(let val): + try val.encode(to: encoder) + case .codeExecutionCallDelta(let val): + try val.encode(to: encoder) + case .codeExecutionResultDelta(let val): + try val.encode(to: encoder) + case .documentDelta(let val): + try val.encode(to: encoder) + case .fileSearchCallDelta(let val): + try val.encode(to: encoder) + case .fileSearchResultDelta(let val): + try val.encode(to: encoder) + case .functionResultDelta(let val): + try val.encode(to: encoder) + case .googleMapsCallDelta(let val): + try val.encode(to: encoder) + case .googleMapsResultDelta(let val): + try val.encode(to: encoder) + case .googleSearchCallDelta(let val): + try val.encode(to: encoder) + case .googleSearchResultDelta(let val): + try val.encode(to: encoder) + case .imageDelta(let val): + try val.encode(to: encoder) + case .mCPServerToolCallDelta(let val): + try val.encode(to: encoder) + case .mCPServerToolResultDelta(let val): + try val.encode(to: encoder) + case .processingCallDelta(let val): + try val.encode(to: encoder) + case .processingResultDelta(let val): + try val.encode(to: encoder) + case .textAnnotationDelta(let val): + try val.encode(to: encoder) + case .textDelta(let val): + try val.encode(to: encoder) + case .thoughtSignatureDelta(let val): + try val.encode(to: encoder) + case .thoughtSummaryDelta(let val): + try val.encode(to: encoder) + case .uRLContextCallDelta(let val): + try val.encode(to: encoder) + case .uRLContextResultDelta(let val): + try val.encode(to: encoder) + case .videoDelta(let val): + try val.encode(to: encoder) + case .unrecognized(let val): + try val.encode(to: encoder) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/StepDeltaMetadata.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/StepDeltaMetadata.swift new file mode 100644 index 00000000000..229293470bf --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/StepDeltaMetadata.swift @@ -0,0 +1,33 @@ +// 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. + +/// Optional metadata accompanying ANY streamed event. +package struct StepDeltaMetadata: Codable, Sendable, Equatable, Hashable { + + /// Statistics on the interaction request's token usage. + package let totalUsage: Usage? + + /// Creates a new `StepDeltaMetadata`. + /// + /// - Parameters: + /// - totalUsage: Statistics on the interaction request's token usage. + package init( + totalUsage: Usage? = nil + ) { + self.totalUsage = totalUsage + } + enum CodingKeys: String, CodingKey { + case totalUsage = "total_usage" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/StepStart.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/StepStart.swift new file mode 100644 index 00000000000..4b4aef5a1bd --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/StepStart.swift @@ -0,0 +1,50 @@ +// 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. + +/// An internal data model for `StepStart`. +package struct StepStart: Codable, Sendable, Equatable, Hashable { + + /// The event_id token to be used to resume the interaction stream, from + /// this event. + package let eventId: String? + + package let eventType: String? + + package let index: Int? + + package let step: Step? + + /// Creates a new `StepStart`. + /// + /// - Parameters: + /// - eventId: The event_id token to be used to resume the interaction stream, from + /// - index: For more details, see ``index``. + /// - step: For more details, see ``step``. + package init( + eventId: String? = nil, + index: Int?, + step: Step? + ) { + self.eventId = eventId + self.eventType = "step.start" + self.index = index + self.step = step + } + enum CodingKeys: String, CodingKey { + case eventId = "event_id" + case eventType = "event_type" + case index = "index" + case step = "step" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/StepStop.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/StepStop.swift new file mode 100644 index 00000000000..d35dc6124de --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/StepStop.swift @@ -0,0 +1,58 @@ +// 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. + +/// An internal data model for `StepStop`. +package struct StepStop: Codable, Sendable, Equatable, Hashable { + + /// The event_id token to be used to resume the interaction stream, from + /// this event. + package let eventId: String? + + package let eventType: String? + + package let index: Int? + + /// Model usage stats for this specific step. + package let stepUsage: Usage? + + /// Cumulative model usage stats from the start of the session. + package let usage: Usage? + + /// Creates a new `StepStop`. + /// + /// - Parameters: + /// - eventId: The event_id token to be used to resume the interaction stream, from + /// - index: For more details, see ``index``. + /// - stepUsage: Model usage stats for this specific step. + /// - usage: Cumulative model usage stats from the start of the session. + package init( + eventId: String? = nil, + index: Int?, + stepUsage: Usage? = nil, + usage: Usage? = nil + ) { + self.eventId = eventId + self.eventType = "step.stop" + self.index = index + self.stepUsage = stepUsage + self.usage = usage + } + enum CodingKeys: String, CodingKey { + case eventId = "event_id" + case eventType = "event_type" + case index = "index" + case stepUsage = "step_usage" + case usage = "usage" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/TextAnnotationDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/TextAnnotationDelta.swift new file mode 100644 index 00000000000..5b8b65683a0 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/TextAnnotationDelta.swift @@ -0,0 +1,37 @@ +// 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. + +/// An internal data model for `TextAnnotationDelta`. +package struct TextAnnotationDelta: Codable, Sendable, Equatable, Hashable { + + /// Citation information for model-generated content. + package let annotations: [Annotation]? + + package let type: String? + + /// Creates a new `TextAnnotationDelta`. + /// + /// - Parameters: + /// - annotations: Citation information for model-generated content. + package init( + annotations: [Annotation]? = nil + ) { + self.annotations = annotations + self.type = "text_annotation_delta" + } + enum CodingKeys: String, CodingKey { + case annotations = "annotations" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/TextContent.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/TextContent.swift new file mode 100644 index 00000000000..8fadf4917ec --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/TextContent.swift @@ -0,0 +1,44 @@ +// 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. + +/// A text content block. +package struct TextContent: Codable, Sendable, Equatable, Hashable { + + /// Citation information for model-generated content. + package let annotations: [Annotation]? + + /// Required. The text content. + package let text: String? + + package let type: String? + + /// Creates a new `TextContent`. + /// + /// - Parameters: + /// - annotations: Citation information for model-generated content. + /// - text: Required. The text content. + package init( + annotations: [Annotation]? = nil, + text: String? + ) { + self.annotations = annotations + self.text = text + self.type = "text" + } + enum CodingKeys: String, CodingKey { + case annotations = "annotations" + case text = "text" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/TextDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/TextDelta.swift new file mode 100644 index 00000000000..e2bfd457b92 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/TextDelta.swift @@ -0,0 +1,36 @@ +// 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. + +/// An internal data model for `TextDelta`. +package struct TextDelta: Codable, Sendable, Equatable, Hashable { + + package let text: String? + + package let type: String? + + /// Creates a new `TextDelta`. + /// + /// - Parameters: + /// - text: For more details, see ``text``. + package init( + text: String? + ) { + self.text = text + self.type = "text" + } + enum CodingKeys: String, CodingKey { + case text = "text" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/TextResponseFormat+MimeType.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/TextResponseFormat+MimeType.swift new file mode 100644 index 00000000000..28c628310df --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/TextResponseFormat+MimeType.swift @@ -0,0 +1,50 @@ +// 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. + +extension TextResponseFormat { + /// The MIME type of the text output. + package enum MimeType: Codable, Sendable, Equatable, Hashable { + + /// JSON output format. + case applicationJson + + /// Plain text output format. + case textPlain + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension TextResponseFormat.MimeType: RawRepresentable { + package var rawValue: String { + switch self { + case .applicationJson: "application/json" + case .textPlain: "text/plain" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "application/json": self = .applicationJson + case "text/plain": self = .textPlain + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/TextResponseFormat.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/TextResponseFormat.swift new file mode 100644 index 00000000000..7daec6fff3c --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/TextResponseFormat.swift @@ -0,0 +1,47 @@ +// 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. + +package import GeminiSharedDataModels + +/// Configuration for text output format. +package struct TextResponseFormat: Codable, Sendable, Equatable, Hashable { + + /// The MIME type of the text output. + package let mimeType: MimeType? + + /// The JSON schema that the output should conform to. Only applicable when + /// mime_type is application/json. + package let schema: [String: JSONValue]? + + package let type: String? + + /// Creates a new `TextResponseFormat`. + /// + /// - Parameters: + /// - mimeType: The MIME type of the text output. + /// - schema: The JSON schema that the output should conform to. Only applicable when + package init( + mimeType: MimeType? = nil, + schema: [String: JSONValue]? = nil + ) { + self.mimeType = mimeType + self.schema = schema + self.type = "text" + } + enum CodingKeys: String, CodingKey { + case mimeType = "mime_type" + case schema = "schema" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ThinkingLevel.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ThinkingLevel.swift new file mode 100644 index 00000000000..ad27fa327bf --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ThinkingLevel.swift @@ -0,0 +1,58 @@ +// 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. + +/// An internal data model for `ThinkingLevel`. +package enum ThinkingLevel: Codable, Sendable, Equatable, Hashable { + + /// Little to no thinking. + case minimal + + /// Low thinking level. + case low + + /// Medium thinking level. + case medium + + /// High thinking level. + case high + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) +} + +// MARK: - RawRepresentable Conformance + +extension ThinkingLevel: RawRepresentable { + package var rawValue: String { + switch self { + case .minimal: "minimal" + case .low: "low" + case .medium: "medium" + case .high: "high" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "minimal": self = .minimal + case "low": self = .low + case "medium": self = .medium + case "high": self = .high + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ThinkingSummaries.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ThinkingSummaries.swift new file mode 100644 index 00000000000..9602a81e211 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ThinkingSummaries.swift @@ -0,0 +1,48 @@ +// 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. + +/// An internal data model for `ThinkingSummaries`. +package enum ThinkingSummaries: Codable, Sendable, Equatable, Hashable { + + /// Auto thinking summaries. + case auto + + /// No thinking summaries. + case none + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) +} + +// MARK: - RawRepresentable Conformance + +extension ThinkingSummaries: RawRepresentable { + package var rawValue: String { + switch self { + case .auto: "auto" + case .none: "none" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "auto": self = .auto + case "none": self = .none + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ThoughtSignatureDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ThoughtSignatureDelta.swift new file mode 100644 index 00000000000..3ae1bca86c4 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ThoughtSignatureDelta.swift @@ -0,0 +1,39 @@ +// 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. + +package import Foundation + +/// An internal data model for `ThoughtSignatureDelta`. +package struct ThoughtSignatureDelta: Codable, Sendable, Equatable, Hashable { + + /// Signature to match the backend source to be part of the generation. + package let signature: Data? + + package let type: String? + + /// Creates a new `ThoughtSignatureDelta`. + /// + /// - Parameters: + /// - signature: Signature to match the backend source to be part of the generation. + package init( + signature: Data? = nil + ) { + self.signature = signature + self.type = "thought_signature" + } + enum CodingKeys: String, CodingKey { + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ThoughtStep.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ThoughtStep.swift new file mode 100644 index 00000000000..458c374949f --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ThoughtStep.swift @@ -0,0 +1,46 @@ +// 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. + +package import Foundation + +/// A thought step. +package struct ThoughtStep: Codable, Sendable, Equatable, Hashable { + + /// A signature hash for backend validation. + package let signature: Data? + + /// A summary of the thought. + package let summary: [ThoughtSummaryContent]? + + package let type: String? + + /// Creates a new `ThoughtStep`. + /// + /// - Parameters: + /// - signature: A signature hash for backend validation. + /// - summary: A summary of the thought. + package init( + signature: Data? = nil, + summary: [ThoughtSummaryContent]? = nil + ) { + self.signature = signature + self.summary = summary + self.type = "thought" + } + enum CodingKeys: String, CodingKey { + case signature = "signature" + case summary = "summary" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ThoughtSummaryContent.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ThoughtSummaryContent.swift new file mode 100644 index 00000000000..d8eec687b88 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ThoughtSummaryContent.swift @@ -0,0 +1,59 @@ +// 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. + +package import GeminiSharedDataModels + +/// An internal data model for `ThoughtSummaryContent`. +package enum ThoughtSummaryContent: Codable, Sendable, Equatable, Hashable { + + /// An image content block. + case imageContent(ImageContent) + + /// A text content block. + case textContent(TextContent) + + /// Unrecognized case. + case unrecognized(JSONValue) + + private enum CodingKeys: String, CodingKey { + case discriminator = "type" + } + + package init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let discValue = try container.decode(String.self, forKey: .discriminator) + switch discValue { + case "image": + let val = try ImageContent(from: decoder) + self = .imageContent(val) + case "text": + let val = try TextContent(from: decoder) + self = .textContent(val) + default: + let val = try JSONValue(from: decoder) + self = .unrecognized(val) + } + } + + package func encode(to encoder: any Encoder) throws { + switch self { + case .imageContent(let val): + try val.encode(to: encoder) + case .textContent(let val): + try val.encode(to: encoder) + case .unrecognized(let val): + try val.encode(to: encoder) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ThoughtSummaryDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ThoughtSummaryDelta.swift new file mode 100644 index 00000000000..bd3514bce2e --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ThoughtSummaryDelta.swift @@ -0,0 +1,37 @@ +// 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. + +/// An internal data model for `ThoughtSummaryDelta`. +package struct ThoughtSummaryDelta: Codable, Sendable, Equatable, Hashable { + + /// A new summary item to be added to the thought. + package let content: Content? + + package let type: String? + + /// Creates a new `ThoughtSummaryDelta`. + /// + /// - Parameters: + /// - content: A new summary item to be added to the thought. + package init( + content: Content? = nil + ) { + self.content = content + self.type = "thought_summary" + } + enum CodingKeys: String, CodingKey { + case content = "content" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/Tool.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/Tool.swift new file mode 100644 index 00000000000..5808ee3e5b8 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/Tool.swift @@ -0,0 +1,107 @@ +// 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. + +package import GeminiSharedDataModels + +/// A tool that can be used by the model. +package enum Tool: Codable, Sendable, Equatable, Hashable { + + /// A tool that can be used by the model to execute code. + case codeExecution(CodeExecution) + + /// A tool that can be used by the model to interact with the computer. + case computerUse(ComputerUse) + + /// A tool that can be used by the model to search files. + case fileSearch(FileSearch) + + /// A tool that can be used by the model. + case function(Function) + + /// A tool that can be used by the model to call Google Maps. + case googleMaps(GoogleMaps) + + /// A tool that can be used by the model to search Google. + case googleSearch(GoogleSearch) + + /// A MCPServer is a server that can be called by the model to perform actions. + case mCPServer(MCPServer) + + /// A tool that can be used by the model to fetch URL context. + case uRLContext(URLContext) + + /// Unrecognized case. + case unrecognized(JSONValue) + + private enum CodingKeys: String, CodingKey { + case discriminator = "type" + } + + package init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let discValue = try container.decode(String.self, forKey: .discriminator) + switch discValue { + case "code_execution": + let val = try CodeExecution(from: decoder) + self = .codeExecution(val) + case "computer_use": + let val = try ComputerUse(from: decoder) + self = .computerUse(val) + case "file_search": + let val = try FileSearch(from: decoder) + self = .fileSearch(val) + case "function": + let val = try Function(from: decoder) + self = .function(val) + case "google_maps": + let val = try GoogleMaps(from: decoder) + self = .googleMaps(val) + case "google_search": + let val = try GoogleSearch(from: decoder) + self = .googleSearch(val) + case "mcp_server": + let val = try MCPServer(from: decoder) + self = .mCPServer(val) + case "url_context": + let val = try URLContext(from: decoder) + self = .uRLContext(val) + default: + let val = try JSONValue(from: decoder) + self = .unrecognized(val) + } + } + + package func encode(to encoder: any Encoder) throws { + switch self { + case .codeExecution(let val): + try val.encode(to: encoder) + case .computerUse(let val): + try val.encode(to: encoder) + case .fileSearch(let val): + try val.encode(to: encoder) + case .function(let val): + try val.encode(to: encoder) + case .googleMaps(let val): + try val.encode(to: encoder) + case .googleSearch(let val): + try val.encode(to: encoder) + case .mCPServer(let val): + try val.encode(to: encoder) + case .uRLContext(let val): + try val.encode(to: encoder) + case .unrecognized(let val): + try val.encode(to: encoder) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/ToolChoiceConfig.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/ToolChoiceConfig.swift new file mode 100644 index 00000000000..16b40a73640 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/ToolChoiceConfig.swift @@ -0,0 +1,33 @@ +// 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. + +/// The tool choice configuration containing allowed tools. +package struct ToolChoiceConfig: Codable, Sendable, Equatable, Hashable { + + /// The allowed tools. + package let allowedTools: AllowedTools? + + /// Creates a new `ToolChoiceConfig`. + /// + /// - Parameters: + /// - allowedTools: The allowed tools. + package init( + allowedTools: AllowedTools? = nil + ) { + self.allowedTools = allowedTools + } + enum CodingKeys: String, CodingKey { + case allowedTools = "allowed_tools" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/TranscriptionConfig.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/TranscriptionConfig.swift new file mode 100644 index 00000000000..ef4ea0dedc5 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/TranscriptionConfig.swift @@ -0,0 +1,76 @@ +// 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. + +package import GeminiSharedDataModels + +/// Configuration for speech recognition (transcription). +package struct TranscriptionConfig: Codable, Sendable, Equatable, Hashable { + + /// Optional. A list of phrases to bias the ASR model towards. + @available(*, deprecated) + package let adaptationPhrases: [String]? + + /// Optional. A list of custom vocabulary phrases to bias the speech recognition model + /// toward recognizing specific terms. + package let customVocabulary: [String]? + + /// Optional. Configures speaker diarization. Supported values: "speaker". + @available(*, deprecated) + package let diarizationMode: String? + + /// Optional. BCP-47 language codes providing hints about the languages present in the + /// audio. If omitted or empty, defaults to automatic language detection. + package let languageCodes: [String]? + + /// Discriminated transcription mode options or enum. + package let mode: JSONValue? + + /// Optional. The granularity of timestamps to include in the transcription output. + /// Supported values: "word". If empty, no timestamps are generated. + @available(*, deprecated) + package let timestampGranularities: [String]? + + /// Creates a new `TranscriptionConfig`. + /// + /// - Parameters: + /// - adaptationPhrases: Optional. A list of phrases to bias the ASR model towards. + /// - customVocabulary: Optional. A list of custom vocabulary phrases to bias the speech recognition model + /// - diarizationMode: Optional. Configures speaker diarization. Supported values: "speaker". + /// - languageCodes: Optional. BCP-47 language codes providing hints about the languages present in the + /// - mode: Discriminated transcription mode options or enum. + /// - timestampGranularities: Optional. The granularity of timestamps to include in the transcription output. + package init( + adaptationPhrases: [String]? = nil, + customVocabulary: [String]? = nil, + diarizationMode: String? = nil, + languageCodes: [String]? = nil, + mode: JSONValue? = nil, + timestampGranularities: [String]? = nil + ) { + self.adaptationPhrases = adaptationPhrases + self.customVocabulary = customVocabulary + self.diarizationMode = diarizationMode + self.languageCodes = languageCodes + self.mode = mode + self.timestampGranularities = timestampGranularities + } + enum CodingKeys: String, CodingKey { + case adaptationPhrases = "adaptation_phrases" + case customVocabulary = "custom_vocabulary" + case diarizationMode = "diarization_mode" + case languageCodes = "language_codes" + case mode = "mode" + case timestampGranularities = "timestamp_granularities" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/TranscriptionMode.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/TranscriptionMode.swift new file mode 100644 index 00000000000..521dab2b4e2 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/TranscriptionMode.swift @@ -0,0 +1,22 @@ +// 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. + +/// Configuration for transcription mode. +package struct TranscriptionMode: Codable, Sendable, Equatable, Hashable { + + /// Creates a new `TranscriptionMode`. + /// + package init() { + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/URLCitation.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/URLCitation.swift new file mode 100644 index 00000000000..0a5fb93aed8 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/URLCitation.swift @@ -0,0 +1,60 @@ +// 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. + +/// A URL citation annotation. +package struct URLCitation: Codable, Sendable, Equatable, Hashable { + + /// End of the attributed segment, exclusive. + package let endIndex: Int? + + /// Start of segment of the response that is attributed to this source. + /// + /// Index indicates the start of the segment, measured in bytes. + package let startIndex: Int? + + /// The title of the URL. + package let title: String? + + package let type: String? + + /// The URL. + package let url: String? + + /// Creates a new `URLCitation`. + /// + /// - Parameters: + /// - endIndex: End of the attributed segment, exclusive. + /// - startIndex: Start of segment of the response that is attributed to this source. + /// - title: The title of the URL. + /// - url: The URL. + package init( + endIndex: Int? = nil, + startIndex: Int? = nil, + title: String? = nil, + url: String? = nil + ) { + self.endIndex = endIndex + self.startIndex = startIndex + self.title = title + self.type = "url_citation" + self.url = url + } + enum CodingKeys: String, CodingKey { + case endIndex = "end_index" + case startIndex = "start_index" + case title = "title" + case type = "type" + case url = "url" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/URLContext.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/URLContext.swift new file mode 100644 index 00000000000..64e0d6a6b16 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/URLContext.swift @@ -0,0 +1,28 @@ +// 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. + +/// A tool that can be used by the model to fetch URL context. +package struct URLContext: Codable, Sendable, Equatable, Hashable { + + package let type: String? + + /// Creates a new `URLContext`. + /// + package init() { + self.type = "url_context" + } + enum CodingKeys: String, CodingKey { + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextCallArguments.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextCallArguments.swift new file mode 100644 index 00000000000..4f89f9f7e20 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextCallArguments.swift @@ -0,0 +1,33 @@ +// 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. + +/// The arguments to pass to the URL context. +package struct URLContextCallArguments: Codable, Sendable, Equatable, Hashable { + + /// The URLs to fetch. + package let urls: [String]? + + /// Creates a new `URLContextCallArguments`. + /// + /// - Parameters: + /// - urls: The URLs to fetch. + package init( + urls: [String]? = nil + ) { + self.urls = urls + } + enum CodingKeys: String, CodingKey { + case urls = "urls" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextCallDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextCallDelta.swift new file mode 100644 index 00000000000..e7531597723 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextCallDelta.swift @@ -0,0 +1,45 @@ +// 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. + +package import Foundation + +/// An internal data model for `URLContextCallDelta`. +package struct URLContextCallDelta: Codable, Sendable, Equatable, Hashable { + + package let arguments: URLContextCallArguments? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `URLContextCallDelta`. + /// + /// - Parameters: + /// - arguments: For more details, see ``arguments``. + /// - signature: A signature hash for backend validation. + package init( + arguments: URLContextCallArguments?, + signature: Data? = nil + ) { + self.arguments = arguments + self.signature = signature + self.type = "url_context_call" + } + enum CodingKeys: String, CodingKey { + case arguments = "arguments" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextCallStep.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextCallStep.swift new file mode 100644 index 00000000000..8ccfa6505d3 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextCallStep.swift @@ -0,0 +1,53 @@ +// 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. + +package import Foundation + +/// URL context call step. +package struct URLContextCallStep: Codable, Sendable, Equatable, Hashable { + + /// Required. The arguments to pass to the URL context. + package let arguments: URLContextCallArguments? + + /// Required. A unique ID for this specific tool call. + package let id: String? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `URLContextCallStep`. + /// + /// - Parameters: + /// - arguments: Required. The arguments to pass to the URL context. + /// - id: Required. A unique ID for this specific tool call. + /// - signature: A signature hash for backend validation. + package init( + arguments: URLContextCallArguments?, + id: String?, + signature: Data? = nil + ) { + self.arguments = arguments + self.id = id + self.signature = signature + self.type = "url_context_call" + } + enum CodingKeys: String, CodingKey { + case arguments = "arguments" + case id = "id" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextResult+Status.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextResult+Status.swift new file mode 100644 index 00000000000..148e06d03fa --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextResult+Status.swift @@ -0,0 +1,60 @@ +// 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. + +extension URLContextResult { + /// The status of the URL retrieval. + package enum Status: Codable, Sendable, Equatable, Hashable { + + /// Url retrieval is successful. + case success + + /// Url retrieval is failed due to error. + case error + + /// Url retrieval is failed because the content is behind paywall. + case paywall + + /// Url retrieval is failed because the content is unsafe. + case unsafe + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension URLContextResult.Status: RawRepresentable { + package var rawValue: String { + switch self { + case .success: "success" + case .error: "error" + case .paywall: "paywall" + case .unsafe: "unsafe" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "success": self = .success + case "error": self = .error + case "paywall": self = .paywall + case "unsafe": self = .unsafe + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextResult.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextResult.swift new file mode 100644 index 00000000000..f5eef70520b --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextResult.swift @@ -0,0 +1,40 @@ +// 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. + +/// The result of the URL context. +package struct URLContextResult: Codable, Sendable, Equatable, Hashable { + + /// The status of the URL retrieval. + package let status: Status? + + /// The URL that was fetched. + package let url: String? + + /// Creates a new `URLContextResult`. + /// + /// - Parameters: + /// - status: The status of the URL retrieval. + /// - url: The URL that was fetched. + package init( + status: Status? = nil, + url: String? = nil + ) { + self.status = status + self.url = url + } + enum CodingKeys: String, CodingKey { + case status = "status" + case url = "url" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextResultDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextResultDelta.swift new file mode 100644 index 00000000000..c38f53ecfbd --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextResultDelta.swift @@ -0,0 +1,51 @@ +// 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. + +package import Foundation + +/// An internal data model for `URLContextResultDelta`. +package struct URLContextResultDelta: Codable, Sendable, Equatable, Hashable { + + package let isError: Bool? + + package let result: [URLContextResult]? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `URLContextResultDelta`. + /// + /// - Parameters: + /// - isError: For more details, see ``isError``. + /// - result: For more details, see ``result``. + /// - signature: A signature hash for backend validation. + package init( + isError: Bool? = nil, + result: [URLContextResult]?, + signature: Data? = nil + ) { + self.isError = isError + self.result = result + self.signature = signature + self.type = "url_context_result" + } + enum CodingKeys: String, CodingKey { + case isError = "is_error" + case result = "result" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextResultStep.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextResultStep.swift new file mode 100644 index 00000000000..336821e8326 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/URLContextResultStep.swift @@ -0,0 +1,60 @@ +// 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. + +package import Foundation + +/// URL context result step. +package struct URLContextResultStep: Codable, Sendable, Equatable, Hashable { + + /// Required. ID to match the ID from the function call block. + package let callId: String? + + /// Whether the URL context resulted in an error. + package let isError: Bool? + + /// Required. The results of the URL context. + package let result: [URLContextResult]? + + /// A signature hash for backend validation. + package let signature: Data? + + package let type: String? + + /// Creates a new `URLContextResultStep`. + /// + /// - Parameters: + /// - callId: Required. ID to match the ID from the function call block. + /// - isError: Whether the URL context resulted in an error. + /// - result: Required. The results of the URL context. + /// - signature: A signature hash for backend validation. + package init( + callId: String?, + isError: Bool? = nil, + result: [URLContextResult]?, + signature: Data? = nil + ) { + self.callId = callId + self.isError = isError + self.result = result + self.signature = signature + self.type = "url_context_result" + } + enum CodingKeys: String, CodingKey { + case callId = "call_id" + case isError = "is_error" + case result = "result" + case signature = "signature" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/Usage.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/Usage.swift new file mode 100644 index 00000000000..fdf8279879b --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/Usage.swift @@ -0,0 +1,104 @@ +// 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. + +/// Statistics on the interaction request's token usage. +package struct Usage: Codable, Sendable, Equatable, Hashable { + + /// A breakdown of cached token usage by modality. + package let cachedTokensByModality: [ModalityTokens]? + + /// Grounding tool count. + package let groundingToolCount: [GroundingToolCount]? + + /// A breakdown of input token usage by modality. + package let inputTokensByModality: [ModalityTokens]? + + /// A breakdown of output token usage by modality. + package let outputTokensByModality: [ModalityTokens]? + + /// A breakdown of tool-use token usage by modality. + package let toolUseTokensByModality: [ModalityTokens]? + + /// Number of tokens in the cached part of the prompt (the cached content). + package let totalCachedTokens: Int? + + /// Number of tokens in the prompt (context). + package let totalInputTokens: Int? + + /// Total number of tokens across all the generated responses. + package let totalOutputTokens: Int? + + /// Number of tokens of thoughts for thinking models. + package let totalThoughtTokens: Int? + + /// Total token count for the interaction request (prompt + responses + other + /// internal tokens). + package let totalTokens: Int? + + /// Number of tokens present in tool-use prompt(s). + package let totalToolUseTokens: Int? + + /// Creates a new `Usage`. + /// + /// - Parameters: + /// - cachedTokensByModality: A breakdown of cached token usage by modality. + /// - groundingToolCount: Grounding tool count. + /// - inputTokensByModality: A breakdown of input token usage by modality. + /// - outputTokensByModality: A breakdown of output token usage by modality. + /// - toolUseTokensByModality: A breakdown of tool-use token usage by modality. + /// - totalCachedTokens: Number of tokens in the cached part of the prompt (the cached content). + /// - totalInputTokens: Number of tokens in the prompt (context). + /// - totalOutputTokens: Total number of tokens across all the generated responses. + /// - totalThoughtTokens: Number of tokens of thoughts for thinking models. + /// - totalTokens: Total token count for the interaction request (prompt + responses + other + /// - totalToolUseTokens: Number of tokens present in tool-use prompt(s). + package init( + cachedTokensByModality: [ModalityTokens]? = nil, + groundingToolCount: [GroundingToolCount]? = nil, + inputTokensByModality: [ModalityTokens]? = nil, + outputTokensByModality: [ModalityTokens]? = nil, + toolUseTokensByModality: [ModalityTokens]? = nil, + totalCachedTokens: Int? = nil, + totalInputTokens: Int? = nil, + totalOutputTokens: Int? = nil, + totalThoughtTokens: Int? = nil, + totalTokens: Int? = nil, + totalToolUseTokens: Int? = nil + ) { + self.cachedTokensByModality = cachedTokensByModality + self.groundingToolCount = groundingToolCount + self.inputTokensByModality = inputTokensByModality + self.outputTokensByModality = outputTokensByModality + self.toolUseTokensByModality = toolUseTokensByModality + self.totalCachedTokens = totalCachedTokens + self.totalInputTokens = totalInputTokens + self.totalOutputTokens = totalOutputTokens + self.totalThoughtTokens = totalThoughtTokens + self.totalTokens = totalTokens + self.totalToolUseTokens = totalToolUseTokens + } + enum CodingKeys: String, CodingKey { + case cachedTokensByModality = "cached_tokens_by_modality" + case groundingToolCount = "grounding_tool_count" + case inputTokensByModality = "input_tokens_by_modality" + case outputTokensByModality = "output_tokens_by_modality" + case toolUseTokensByModality = "tool_use_tokens_by_modality" + case totalCachedTokens = "total_cached_tokens" + case totalInputTokens = "total_input_tokens" + case totalOutputTokens = "total_output_tokens" + case totalThoughtTokens = "total_thought_tokens" + case totalTokens = "total_tokens" + case totalToolUseTokens = "total_tool_use_tokens" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/UserInputStep.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/UserInputStep.swift new file mode 100644 index 00000000000..32472d5de97 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/UserInputStep.swift @@ -0,0 +1,36 @@ +// 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. + +/// Input provided by the user. +package struct UserInputStep: Codable, Sendable, Equatable, Hashable { + + package let content: [Content]? + + package let type: String? + + /// Creates a new `UserInputStep`. + /// + /// - Parameters: + /// - content: For more details, see ``content``. + package init( + content: [Content]? = nil + ) { + self.content = content + self.type = "user_input" + } + enum CodingKeys: String, CodingKey { + case content = "content" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/VerbatimTranscriptionMode.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/VerbatimTranscriptionMode.swift new file mode 100644 index 00000000000..ea567847d38 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/VerbatimTranscriptionMode.swift @@ -0,0 +1,45 @@ +// 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. + +/// Configuration for verbatim transcription mode. +package struct VerbatimTranscriptionMode: Codable, Sendable, Equatable, Hashable { + + /// Optional. Configures speaker diarization. Supported values: "speaker". + package let diarizationMode: String? + + /// Optional. The granularity of timestamps to include in the transcription output. + /// Supported values: "word". If empty, no timestamps are generated. + package let timestampGranularities: [String]? + + package let type: String? + + /// Creates a new `VerbatimTranscriptionMode`. + /// + /// - Parameters: + /// - diarizationMode: Optional. Configures speaker diarization. Supported values: "speaker". + /// - timestampGranularities: Optional. The granularity of timestamps to include in the transcription output. + package init( + diarizationMode: String? = nil, + timestampGranularities: [String]? = nil + ) { + self.diarizationMode = diarizationMode + self.timestampGranularities = timestampGranularities + self.type = "verbatim" + } + enum CodingKeys: String, CodingKey { + case diarizationMode = "diarization_mode" + case timestampGranularities = "timestamp_granularities" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/VideoConfig+Task.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoConfig+Task.swift new file mode 100644 index 00000000000..0c8ea455db9 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoConfig+Task.swift @@ -0,0 +1,69 @@ +// 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. + +extension VideoConfig { + /// Optional task mode for video generation. If not specified, the model + /// automatically determines the appropriate mode based on the provided text + /// prompt and input media. + package enum Task: Codable, Sendable, Equatable, Hashable { + + /// Generates video solely from a text prompt. + case textToVideo + + /// Generates video from one or two source images. The first image defines + /// the starting frame, and the optional second image defines the ending + /// frame. + case imageToVideo + + /// Generates video using reference media (such as images, audio, or video). + case referenceToVideo + + /// Modifies an existing input video. + case edit + + /// Extends an existing input video. + case extend + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension VideoConfig.Task: RawRepresentable { + package var rawValue: String { + switch self { + case .textToVideo: "text_to_video" + case .imageToVideo: "image_to_video" + case .referenceToVideo: "reference_to_video" + case .edit: "edit" + case .extend: "extend" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "text_to_video": self = .textToVideo + case "image_to_video": self = .imageToVideo + case "reference_to_video": self = .referenceToVideo + case "edit": self = .edit + case "extend": self = .extend + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/VideoConfig.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoConfig.swift new file mode 100644 index 00000000000..86a42f34a56 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoConfig.swift @@ -0,0 +1,35 @@ +// 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. + +/// Configuration options for video generation. +package struct VideoConfig: Codable, Sendable, Equatable, Hashable { + + /// Optional task mode for video generation. If not specified, the model + /// automatically determines the appropriate mode based on the provided text + /// prompt and input media. + package let task: Task? + + /// Creates a new `VideoConfig`. + /// + /// - Parameters: + /// - task: Optional task mode for video generation. If not specified, the model + package init( + task: Task? = nil + ) { + self.task = task + } + enum CodingKeys: String, CodingKey { + case task = "task" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/VideoContent+MimeType.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoContent+MimeType.swift new file mode 100644 index 00000000000..8d2ef9eb9b2 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoContent+MimeType.swift @@ -0,0 +1,85 @@ +// 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. + +extension VideoContent { + /// The mime type of the video. + package enum MimeType: Codable, Sendable, Equatable, Hashable { + + /// MP4 video format + case mp4 + + /// MPEG video format + case mpeg + + /// MPG video format + case mpg + + /// MOV video format + case mov + + /// AVI video format + case avi + + /// FLV video format + case xFlv + + /// WebM video format + case webm + + /// WMV video format + case wmv + + /// 3GPP video format + case threeGpp + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension VideoContent.MimeType: RawRepresentable { + package var rawValue: String { + switch self { + case .mp4: "video/mp4" + case .mpeg: "video/mpeg" + case .mpg: "video/mpg" + case .mov: "video/mov" + case .avi: "video/avi" + case .xFlv: "video/x-flv" + case .webm: "video/webm" + case .wmv: "video/wmv" + case .threeGpp: "video/3gpp" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "video/mp4": self = .mp4 + case "video/mpeg": self = .mpeg + case "video/mpg": self = .mpg + case "video/mov": self = .mov + case "video/avi": self = .avi + case "video/x-flv": self = .xFlv + case "video/webm": self = .webm + case "video/wmv": self = .wmv + case "video/3gpp": self = .threeGpp + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/VideoContent.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoContent.swift new file mode 100644 index 00000000000..806801f18a2 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoContent.swift @@ -0,0 +1,76 @@ +// 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. + +package import Foundation +package import GeminiSharedDataModels + +/// A video content block. +package struct VideoContent: Codable, Sendable, Equatable, Hashable { + + /// The video content. + package let data: Data? + + /// The mime type of the video. + package let mimeType: MimeType? + + /// A user-defined name for this content block. Can be referenced by the model + /// in the final response. + package let name: String? + + /// How the model processes this video for understanding. + package let processing: JSONValue? + + /// The resolution of the media. + package let resolution: MediaResolution? + + package let type: String? + + /// The URI of the video. + package let uri: String? + + /// Creates a new `VideoContent`. + /// + /// - Parameters: + /// - data: The video content. + /// - mimeType: The mime type of the video. + /// - name: A user-defined name for this content block. Can be referenced by the model + /// - processing: How the model processes this video for understanding. + /// - resolution: The resolution of the media. + /// - uri: The URI of the video. + package init( + data: Data? = nil, + mimeType: MimeType? = nil, + name: String? = nil, + processing: JSONValue? = nil, + resolution: MediaResolution? = nil, + uri: String? = nil + ) { + self.data = data + self.mimeType = mimeType + self.name = name + self.processing = processing + self.resolution = resolution + self.type = "video" + self.uri = uri + } + enum CodingKeys: String, CodingKey { + case data = "data" + case mimeType = "mime_type" + case name = "name" + case processing = "processing" + case resolution = "resolution" + case type = "type" + case uri = "uri" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/VideoDelta+MimeType.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoDelta+MimeType.swift new file mode 100644 index 00000000000..ea5bdbbf97f --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoDelta+MimeType.swift @@ -0,0 +1,89 @@ +// 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. + +extension VideoDelta { + package enum MimeType: Codable, Sendable, Equatable, Hashable { + + /// MP4 video format + case mp4 + + /// MPEG video format + case mpeg + + /// MPG video format + case mpg + + /// MOV video format + case mov + + /// AVI video format + case avi + + /// FLV video format + case xFlv + + /// WebM video format + case webm + + /// WMV video format + case wmv + + /// 3GPP video format + case threeGpp + + /// JPEG 2000 video format + case jpeg2000 + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension VideoDelta.MimeType: RawRepresentable { + package var rawValue: String { + switch self { + case .mp4: "video/mp4" + case .mpeg: "video/mpeg" + case .mpg: "video/mpg" + case .mov: "video/mov" + case .avi: "video/avi" + case .xFlv: "video/x-flv" + case .webm: "video/webm" + case .wmv: "video/wmv" + case .threeGpp: "video/3gpp" + case .jpeg2000: "video/jpeg2000" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "video/mp4": self = .mp4 + case "video/mpeg": self = .mpeg + case "video/mpg": self = .mpg + case "video/mov": self = .mov + case "video/avi": self = .avi + case "video/x-flv": self = .xFlv + case "video/webm": self = .webm + case "video/wmv": self = .wmv + case "video/3gpp": self = .threeGpp + case "video/jpeg2000": self = .jpeg2000 + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/VideoDelta.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoDelta.swift new file mode 100644 index 00000000000..ab91b9bb929 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoDelta.swift @@ -0,0 +1,57 @@ +// 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. + +package import Foundation + +/// An internal data model for `VideoDelta`. +package struct VideoDelta: Codable, Sendable, Equatable, Hashable { + + package let data: Data? + + package let mimeType: MimeType? + + /// The resolution of the media. + package let resolution: MediaResolution? + + package let type: String? + + package let uri: String? + + /// Creates a new `VideoDelta`. + /// + /// - Parameters: + /// - data: For more details, see ``data``. + /// - mimeType: For more details, see ``mimeType``. + /// - resolution: The resolution of the media. + /// - uri: For more details, see ``uri``. + package init( + data: Data? = nil, + mimeType: MimeType? = nil, + resolution: MediaResolution? = nil, + uri: String? = nil + ) { + self.data = data + self.mimeType = mimeType + self.resolution = resolution + self.type = "video" + self.uri = uri + } + enum CodingKeys: String, CodingKey { + case data = "data" + case mimeType = "mime_type" + case resolution = "resolution" + case type = "type" + case uri = "uri" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/VideoResponseFormat+AspectRatio.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoResponseFormat+AspectRatio.swift new file mode 100644 index 00000000000..6161681f1cb --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoResponseFormat+AspectRatio.swift @@ -0,0 +1,50 @@ +// 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. + +extension VideoResponseFormat { + /// The aspect ratio for the video output. + package enum AspectRatio: Codable, Sendable, Equatable, Hashable { + + /// 16:9 aspect ratio. + case sixteenByNine + + /// 9:16 aspect ratio. + case nineBySixteen + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension VideoResponseFormat.AspectRatio: RawRepresentable { + package var rawValue: String { + switch self { + case .sixteenByNine: "16:9" + case .nineBySixteen: "9:16" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "16:9": self = .sixteenByNine + case "9:16": self = .nineBySixteen + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/VideoResponseFormat+Delivery.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoResponseFormat+Delivery.swift new file mode 100644 index 00000000000..c26feab640e --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoResponseFormat+Delivery.swift @@ -0,0 +1,50 @@ +// 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. + +extension VideoResponseFormat { + /// The delivery mode for the video output. + package enum Delivery: Codable, Sendable, Equatable, Hashable { + + /// Video data is returned inline in the response. + case inline + + /// Video data is returned as a URI. + case uri + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension VideoResponseFormat.Delivery: RawRepresentable { + package var rawValue: String { + switch self { + case .inline: "inline" + case .uri: "uri" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "inline": self = .inline + case "uri": self = .uri + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/VideoResponseFormat+Resolution.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoResponseFormat+Resolution.swift new file mode 100644 index 00000000000..2783e8b6303 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoResponseFormat+Resolution.swift @@ -0,0 +1,60 @@ +// 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. + +extension VideoResponseFormat { + /// The video output resolution. Defaults to 720p. + package enum Resolution: Codable, Sendable, Equatable, Hashable { + + /// 360p resolution. + case three60p + + /// 720p resolution. + case seven20p + + /// 1080p resolution. + case ten80p + + /// 4K resolution. + case fourK + + /// Unrecognized case. + /// + /// - Parameter value: The raw string value of the unrecognized enum case. + case unrecognized(_ value: String) + } +} + +// MARK: - RawRepresentable Conformance + +extension VideoResponseFormat.Resolution: RawRepresentable { + package var rawValue: String { + switch self { + case .three60p: "360p" + case .seven20p: "720p" + case .ten80p: "1080p" + case .fourK: "4k" + case .unrecognized(let value): value + } + } + + package init(rawValue: String) { + switch rawValue { + case "360p": self = .three60p + case "720p": self = .seven20p + case "1080p": self = .ten80p + case "4k": self = .fourK + default: self = .unrecognized(rawValue) + } + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/VideoResponseFormat.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoResponseFormat.swift new file mode 100644 index 00000000000..0e93cb2174c --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/VideoResponseFormat.swift @@ -0,0 +1,58 @@ +// 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. + +/// Configuration for video output format. +package struct VideoResponseFormat: Codable, Sendable, Equatable, Hashable { + + /// The aspect ratio for the video output. + package let aspectRatio: AspectRatio? + + /// The delivery mode for the video output. + package let delivery: Delivery? + + /// The duration for the video output. + package let duration: String? + + /// The video output resolution. Defaults to 720p. + package let resolution: Resolution? + + package let type: String? + + /// Creates a new `VideoResponseFormat`. + /// + /// - Parameters: + /// - aspectRatio: The aspect ratio for the video output. + /// - delivery: The delivery mode for the video output. + /// - duration: The duration for the video output. + /// - resolution: The video output resolution. Defaults to 720p. + package init( + aspectRatio: AspectRatio? = nil, + delivery: Delivery? = nil, + duration: String? = nil, + resolution: Resolution? = nil + ) { + self.aspectRatio = aspectRatio + self.delivery = delivery + self.duration = duration + self.resolution = resolution + self.type = "video" + } + enum CodingKeys: String, CodingKey { + case aspectRatio = "aspect_ratio" + case delivery = "delivery" + case duration = "duration" + case resolution = "resolution" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/WebhookConfig.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/WebhookConfig.swift new file mode 100644 index 00000000000..e9c4f553dae --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/WebhookConfig.swift @@ -0,0 +1,44 @@ +// 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. + +package import GeminiSharedDataModels + +/// Message for configuring webhook events for a request. +package struct WebhookConfig: Codable, Sendable, Equatable, Hashable { + + /// Optional. If set, these webhook URIs will be used for webhook events instead of the + /// registered webhooks. + package let uris: [String]? + + /// Optional. The user metadata that will be returned on each event emission to the + /// webhooks. + package let userMetadata: [String: JSONValue]? + + /// Creates a new `WebhookConfig`. + /// + /// - Parameters: + /// - uris: Optional. If set, these webhook URIs will be used for webhook events instead of the + /// - userMetadata: Optional. The user metadata that will be returned on each event emission to the + package init( + uris: [String]? = nil, + userMetadata: [String: JSONValue]? = nil + ) { + self.uris = uris + self.userMetadata = userMetadata + } + enum CodingKeys: String, CodingKey { + case uris = "uris" + case userMetadata = "user_metadata" + } +} diff --git a/GeminiLanguageModel/Sources/InteractionsDataModels/WordInfo.swift b/GeminiLanguageModel/Sources/InteractionsDataModels/WordInfo.swift new file mode 100644 index 00000000000..18095363e00 --- /dev/null +++ b/GeminiLanguageModel/Sources/InteractionsDataModels/WordInfo.swift @@ -0,0 +1,78 @@ +// 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. + +/// Word-level ASR annotation for transcription output. +/// Carries the word text, optional timing, and optional speaker attribution. +package struct WordInfo: Codable, Sendable, Equatable, Hashable { + + /// End of the attributed segment, exclusive. + package let endIndex: Int? + + /// End offset in time of the word relative to the start of the audio. + /// Present when timestamp_granularities contains "word". + package let endOffset: String? + + /// Optional. Speaker label for this word (e.g. "spk_1", "spk_2"). + /// Present when diarization_mode is set in TranscriptionConfig. + package let speaker: String? + + /// Start of segment of the response that is attributed to this source. + /// + /// Index indicates the start of the segment, measured in bytes. + package let startIndex: Int? + + /// Start offset in time of the word relative to the start of the audio. + /// Present when timestamp_granularities contains "word". + package let startOffset: String? + + /// The transcribed word. + package let text: String? + + package let type: String? + + /// Creates a new `WordInfo`. + /// + /// - Parameters: + /// - endIndex: End of the attributed segment, exclusive. + /// - endOffset: End offset in time of the word relative to the start of the audio. + /// - speaker: Optional. Speaker label for this word (e.g. "spk_1", "spk_2"). + /// - startIndex: Start of segment of the response that is attributed to this source. + /// - startOffset: Start offset in time of the word relative to the start of the audio. + /// - text: The transcribed word. + package init( + endIndex: Int? = nil, + endOffset: String? = nil, + speaker: String? = nil, + startIndex: Int? = nil, + startOffset: String? = nil, + text: String? = nil + ) { + self.endIndex = endIndex + self.endOffset = endOffset + self.speaker = speaker + self.startIndex = startIndex + self.startOffset = startOffset + self.text = text + self.type = "word_info" + } + enum CodingKeys: String, CodingKey { + case endIndex = "end_index" + case endOffset = "end_offset" + case speaker = "speaker" + case startIndex = "start_index" + case startOffset = "start_offset" + case text = "text" + case type = "type" + } +} diff --git a/GeminiLanguageModel/Tests/GeminiAPIClientTests/GeminiAPIClientIntegrationTests.swift b/GeminiLanguageModel/Tests/GeminiAPIClientTests/GeminiAPIClientIntegrationTests.swift index 2f61f85dd5d..334658f7711 100644 --- a/GeminiLanguageModel/Tests/GeminiAPIClientTests/GeminiAPIClientIntegrationTests.swift +++ b/GeminiLanguageModel/Tests/GeminiAPIClientTests/GeminiAPIClientIntegrationTests.swift @@ -14,6 +14,7 @@ import Foundation import GeminiAPIDataModels +import GeminiSharedDataModels import GeminiTestUtilities import Testing diff --git a/GeminiLanguageModel/Tests/GeminiAPIClientTests/GeminiAPIClientTests.swift b/GeminiLanguageModel/Tests/GeminiAPIClientTests/GeminiAPIClientTests.swift index f23f916f2de..4e17b465f6b 100644 --- a/GeminiLanguageModel/Tests/GeminiAPIClientTests/GeminiAPIClientTests.swift +++ b/GeminiLanguageModel/Tests/GeminiAPIClientTests/GeminiAPIClientTests.swift @@ -14,7 +14,9 @@ import Foundation import GeminiAPIDataModels +import GeminiSharedDataModels import GeminiTestUtilities +import InteractionsDataModels import Testing @testable import GeminiAPIClient @@ -929,6 +931,195 @@ struct GeminiAPIClientTests { #expect(response.totalTokens == 128) } + // MARK: - Interactions API Tests + + @Test + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + func makeInteractionsRequestURLDirectDeveloperAPI() throws { + let client = makeClient() + + let url = try client.makeInteractionsRequestURL() + + #expect( + url.absoluteString + == "https://generativelanguage.googleapis.com/v1beta/\(testID)/interactions") + } + + @Test + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + func makeInteractionsRequestURLFirebaseAILogic() throws { + let firebaseResource = ModelResource( + modelID: "gemini-3.5-flash-lite", + urlResourceName: "projects/my-firebase-project/models/gemini-3.5-flash-lite", + payloadResourceName: "models/gemini-3.5-flash-lite" + ) + let firebaseEndpoint = EndpointConfiguration( + host: "firebasevertexai.googleapis.com", + apiVersion: "v1beta" + ) + let client = makeClient( + modelResource: firebaseResource, + endpointConfiguration: firebaseEndpoint + ) + + let url = try client.makeInteractionsRequestURL() + + #expect( + url.absoluteString + == "https://firebasevertexai.googleapis.com/v1beta/projects/my-firebase-project/interactions" + ) + } + + @Test + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + func makeInteractionsRequestURLAgentPlatform() throws { + let agentPlatformResource = ModelResource( + modelID: "gemini-3.5-flash-lite", + urlResourceName: + "projects/my-project/locations/global/publishers/google/models/gemini-3.5-flash-lite", + payloadResourceName: "publishers/google/models/gemini-3.5-flash-lite" + ) + let agentPlatformEndpoint = EndpointConfiguration( + host: "generativelanguage.googleapis.com", + apiVersion: "v1beta1" + ) + let client = makeClient( + modelResource: agentPlatformResource, + endpointConfiguration: agentPlatformEndpoint + ) + + let url = try client.makeInteractionsRequestURL() + + #expect( + url.absoluteString + == "https://generativelanguage.googleapis.com/v1beta1/projects/my-project/locations/global/interactions" + ) + } + + @Test + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + func interactionStreamSimplePrompt() async throws { + let client = makeClient() + let expectedURL = try makeExpectedInteractionsURL() + let httpResponse = try makeResponse( + url: expectedURL, + headerFields: ["Content-Type": "text/event-stream"] + ) + let ssePayload = """ + data: {"event_type": "step.delta", "index": 0, "delta": {"type": "text", "text": "Hello from Interactions!"}} + + data: {"event_type": "interaction.completed", "interaction": {"id": "int_123", "status": "completed", "usage": {"total_input_tokens": 10, "total_output_tokens": 5, "total_tokens": 15}}} + + """ + + MockHTTPURLProtocol.setHandler(for: expectedURL) { request, proto in + #expect(request.value(forHTTPHeaderField: "Content-Type") == "application/json") + #expect(request.value(forHTTPHeaderField: "Accept") == "text/event-stream") + proto.client?.urlProtocol(proto, didReceive: httpResponse, cacheStoragePolicy: .notAllowed) + proto.client?.urlProtocol(proto, didLoad: Data(ssePayload.utf8)) + proto.client?.urlProtocolDidFinishLoading(proto) + } + + let request = CreateModelInteraction( + input: .string("Say hello"), + model: Model(rawValue: "gemini-3.5-flash-lite"), + stream: true + ) + let stream = try await client.interactionStream(for: request) + var events: [InteractionSSEEvent] = [] + for try await event in stream { + events.append(event) + } + + #expect(events.count == 2) + guard case .stepDelta(let stepDelta) = events[0] else { + Issue.record("Expected stepDelta event") + return + } + #expect(stepDelta.index == 0) + guard case .textDelta(let textDelta) = stepDelta.delta else { + Issue.record("Expected textDelta data") + return + } + #expect(textDelta.text == "Hello from Interactions!") + + guard case .interactionCompletedEvent(let completed) = events[1] else { + Issue.record("Expected interactionCompleted event") + return + } + #expect(completed.interaction?.usage?.totalTokens == 15) + } + + @Test + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + func interactionStreamWithDoneSentinel() async throws { + let client = makeClient() + let expectedURL = try makeExpectedInteractionsURL() + let httpResponse = try makeResponse( + url: expectedURL, + headerFields: ["Content-Type": "text/event-stream"] + ) + let ssePayload = """ + data: {"event_type": "step.delta", "index": 0, "delta": {"type": "text", "text": "Streaming..."}} + + data: [DONE] + + """ + + MockHTTPURLProtocol.setHandler(for: expectedURL) { _, proto in + proto.client?.urlProtocol(proto, didReceive: httpResponse, cacheStoragePolicy: .notAllowed) + proto.client?.urlProtocol(proto, didLoad: Data(ssePayload.utf8)) + proto.client?.urlProtocolDidFinishLoading(proto) + } + + let request = CreateModelInteraction( + input: .string("Stream test"), + model: Model(rawValue: "gemini-3.5-flash-lite"), + stream: true + ) + let stream = try await client.interactionStream(for: request) + var events: [InteractionSSEEvent] = [] + for try await event in stream { + events.append(event) + } + + #expect(events.count == 1) + } + + @Test + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + func interactionStreamMidStreamError() async throws { + let client = makeClient() + let expectedURL = try makeExpectedInteractionsURL() + let httpResponse = try makeResponse( + url: expectedURL, + headerFields: ["Content-Type": "text/event-stream"] + ) + let ssePayload = """ + data: {"event_type": "step.delta", "index": 0, "delta": {"type": "text", "text": "Starting..."}} + + data: {"error": {"code": 429, "message": "Resource exhausted", "status": "RESOURCE_EXHAUSTED"}} + + """ + + MockHTTPURLProtocol.setHandler(for: expectedURL) { _, proto in + proto.client?.urlProtocol(proto, didReceive: httpResponse, cacheStoragePolicy: .notAllowed) + proto.client?.urlProtocol(proto, didLoad: Data(ssePayload.utf8)) + proto.client?.urlProtocolDidFinishLoading(proto) + } + + let request = CreateModelInteraction( + input: .string("Error test"), + model: Model(rawValue: "gemini-3.5-flash-lite"), + stream: true + ) + let stream = try await client.interactionStream(for: request) + + await #expect(throws: GeminiAPIError.self) { + for try await _ in stream {} + } + } + // MARK: - Test Helpers private func makePromptRequest(_ prompt: String) -> GenerateContentRequest { @@ -995,4 +1186,28 @@ struct GeminiAPIClientTests { } return try client.makeRequestURL(action: action, queryItems: queryItems) } + + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + private func makeExpectedInteractionsURL( + modelResource: ModelResource = defaultModelResource, + endpointConfiguration: EndpointConfiguration? = nil, + query: String? = nil + ) throws -> URL { + let client = makeClient( + modelResource: modelResource, + endpointConfiguration: endpointConfiguration + ) + let queryItems: [URLQueryItem]? + if let query { + let parts = query.split(separator: "=", maxSplits: 1) + if parts.count == 2 { + queryItems = [URLQueryItem(name: String(parts[0]), value: String(parts[1]))] + } else { + queryItems = [URLQueryItem(name: query, value: nil)] + } + } else { + queryItems = nil + } + return try client.makeInteractionsRequestURL(queryItems: queryItems) + } } diff --git a/GeminiLanguageModel/Tests/GeminiAPIClientTests/GeminiAPIErrorTests.swift b/GeminiLanguageModel/Tests/GeminiAPIClientTests/GeminiAPIErrorTests.swift index 794eca338ad..de5713a9215 100644 --- a/GeminiLanguageModel/Tests/GeminiAPIClientTests/GeminiAPIErrorTests.swift +++ b/GeminiLanguageModel/Tests/GeminiAPIClientTests/GeminiAPIErrorTests.swift @@ -14,6 +14,7 @@ import Foundation import GeminiAPIDataModels +import GeminiSharedDataModels import Testing @testable import GeminiAPIClient diff --git a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiErrorMapperTests.swift b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiErrorMapperTests.swift index de73dcd5a24..8c104a7aa38 100644 --- a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiErrorMapperTests.swift +++ b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiErrorMapperTests.swift @@ -17,6 +17,7 @@ import FoundationModels import GeminiAPIClient import GeminiAPIDataModels + import GeminiSharedDataModels import GeminiTestUtilities import Testing diff --git a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiInteractionsRequestTranslatorTests.swift b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiInteractionsRequestTranslatorTests.swift new file mode 100644 index 00000000000..03f376ef24a --- /dev/null +++ b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiInteractionsRequestTranslatorTests.swift @@ -0,0 +1,189 @@ +// 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 canImport(FoundationModels) && compiler(>=6.4) + import Foundation + import FoundationModels + import GeminiAPIClient + import GeminiSharedDataModels + import GeminiTestUtilities + import InteractionsDataModels + import Testing + + @testable import GeminiLanguageModel + + @Suite("GeminiInteractionsRequestTranslator Tests", .requireFoundationModels) + struct GeminiInteractionsRequestTranslatorTests { + @Generable(description: "A simple user profile") + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + @available(tvOS, unavailable) + struct UserProfile { + var username: String + var score: Int + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func translatesRequestWithoutSchema() throws { + let promptEntry = Transcript.Entry.prompt( + Transcript.Prompt( + id: "prompt-1", + segments: [.text(Transcript.TextSegment(content: "Hello"))] + ) + ) + let transcript = Transcript(entries: [promptEntry]) + let request = LanguageModelExecutorGenerationRequest( + id: UUID(), + transcript: transcript, + enabledTools: [], + schema: nil, + generationOptions: GenerationOptions(), + contextOptions: ContextOptions(), + metadata: [:] + ) + + let result = try GeminiInteractionsRequestTranslator.translate( + request, + modelResource: .gemini38Flash + ) + + #expect(result.systemInstruction == nil) + #expect(result.model?.rawValue == "gemini-3.8-flash") + #expect(result.store == false) + #expect(result.stream == true) + #expect(result.tools == nil) + #expect(result.responseFormat == nil) + guard case .stepList(let steps) = result.input else { + Issue.record("Expected stepList input") + return + } + #expect(steps.count == 1) + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func translatesRequestWithSchema() throws { + let promptEntry = Transcript.Entry.prompt( + Transcript.Prompt( + id: "prompt-1", + segments: [.text(Transcript.TextSegment(content: "Generate profile"))] + ) + ) + let transcript = Transcript(entries: [promptEntry]) + let schema = UserProfile.generationSchema + let request = LanguageModelExecutorGenerationRequest( + id: UUID(), + transcript: transcript, + enabledTools: [], + schema: schema, + generationOptions: GenerationOptions(), + contextOptions: ContextOptions(), + metadata: [:] + ) + + let result = try GeminiInteractionsRequestTranslator.translate( + request, + modelResource: .gemini38Flash + ) + + let responseFormat = try #require(result.responseFormat) + guard case .object(let schemaObject) = responseFormat else { + Issue.record("Expected responseFormat to be JSONValue.object") + return + } + #expect(schemaObject["type"] == .string("object")) + #expect(schemaObject["properties"] != nil) + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func translatesRequestWithToolsAndToolCallingMode() throws { + let toolDefinition = Transcript.ToolDefinition( + name: "test_tool", + description: "A test tool", + parameters: UserProfile.generationSchema + ) + let promptEntry = Transcript.Entry.prompt( + Transcript.Prompt( + id: "prompt-1", + segments: [.text(Transcript.TextSegment(content: "Call tool"))] + ) + ) + let transcript = Transcript( + entries: [ + .instructions( + Transcript.Instructions( + segments: [], + toolDefinitions: [toolDefinition] + ) + ), + promptEntry, + ] + ) + let request = LanguageModelExecutorGenerationRequest( + id: UUID(), + transcript: transcript, + enabledTools: [toolDefinition], + schema: nil, + generationOptions: GenerationOptions(toolCallingMode: .required), + contextOptions: ContextOptions(), + metadata: [:] + ) + + let result = try GeminiInteractionsRequestTranslator.translate( + request, + modelResource: .gemini38Flash + ) + + let tools = try #require(result.tools) + #expect(tools.count == 1) + guard case .function(let fn) = tools.first else { + Issue.record("Expected function tool") + return + } + #expect(fn.name == "test_tool") + #expect(fn.description == "A test tool") + #expect(result.generationConfig?.toolChoice == JSONValue.string("any")) + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func usesPayloadResourceNameWhenPrefixedWithPublishers() throws { + let modelResource = ModelResource( + modelID: "gemini-3.8-flash", + urlResourceName: "projects/p/locations/l/publishers/google/models/gemini-3.8-flash", + payloadResourceName: "publishers/google/models/gemini-3.8-flash" + ) + let transcript = Transcript(entries: [ + .prompt(Transcript.Prompt(segments: [.text(Transcript.TextSegment(content: "Hi"))])) + ]) + let request = LanguageModelExecutorGenerationRequest( + id: UUID(), + transcript: transcript, + enabledTools: [], + schema: nil, + generationOptions: GenerationOptions(), + contextOptions: ContextOptions(), + metadata: [:] + ) + + let result = try GeminiInteractionsRequestTranslator.translate( + request, + modelResource: modelResource + ) + + #expect(result.model?.rawValue == "publishers/google/models/gemini-3.8-flash") + } + } +#endif diff --git a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiInteractionsTranscriptTranslatorTests.swift b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiInteractionsTranscriptTranslatorTests.swift new file mode 100644 index 00000000000..c232caf75a6 --- /dev/null +++ b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiInteractionsTranscriptTranslatorTests.swift @@ -0,0 +1,205 @@ +// 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 canImport(FoundationModels) && compiler(>=6.4) + import Foundation + import FoundationModels + import GeminiSharedDataModels + import GeminiTestUtilities + import InteractionsDataModels + import Testing + + @testable import GeminiLanguageModel + + @Suite("GeminiInteractionsTranscriptTranslator Tests", .requireFoundationModels) + struct GeminiInteractionsTranscriptTranslatorTests { + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func translatesInstructionsAndPrompt() throws { + let transcript = Transcript( + entries: [ + .instructions( + Transcript.Instructions( + segments: [.text(Transcript.TextSegment(content: "You are a helpful assistant."))], + toolDefinitions: [] + ) + ), + .prompt( + Transcript.Prompt( + segments: [.text(Transcript.TextSegment(content: "Hello!"))] + ) + ), + ] + ) + + let result = try GeminiInteractionsTranscriptTranslator.translate(transcript) + + #expect(result.systemInstruction == "You are a helpful assistant.") + #expect(result.steps.count == 1) + guard case .userInputStep(let userInput) = result.steps.first else { + Issue.record("Expected userInputStep") + return + } + let content = try #require(userInput.content?.first) + guard case .textContent(let textContent) = content else { + Issue.record("Expected textContent") + return + } + #expect(textContent.text == "Hello!") + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func translatesMultiTurnConversation() throws { + let signatureData = Data("sig-123".utf8) + let transcript = Transcript( + entries: [ + .prompt( + Transcript.Prompt( + segments: [.text(Transcript.TextSegment(content: "Explain gravity."))] + ) + ), + .reasoning( + Transcript.Reasoning( + segments: [.text(Transcript.TextSegment(content: "Physics thought process..."))], + signature: signatureData + ) + ), + .response( + Transcript.Response( + segments: [.text(Transcript.TextSegment(content: "Gravity is a fundamental force."))] + ) + ), + ] + ) + + let result = try GeminiInteractionsTranscriptTranslator.translate(transcript) + + #expect(result.systemInstruction == nil) + #expect(result.steps.count == 3) + + guard case .userInputStep(let userStep) = result.steps[0] else { + Issue.record("Expected userInputStep at index 0") + return + } + guard case .textContent(let userText) = userStep.content?.first else { + Issue.record("Expected textContent in userStep") + return + } + #expect(userText.text == "Explain gravity.") + + guard case .thoughtStep(let thoughtStep) = result.steps[1] else { + Issue.record("Expected thoughtStep at index 1") + return + } + #expect(thoughtStep.signature == signatureData) + guard case .textContent(let thoughtText) = thoughtStep.summary?.first else { + Issue.record("Expected textContent in thoughtStep summary") + return + } + #expect(thoughtText.text == "Physics thought process...") + + guard case .modelOutputStep(let modelStep) = result.steps[2] else { + Issue.record("Expected modelOutputStep at index 2") + return + } + guard case .textContent(let modelText) = modelStep.content?.first else { + Issue.record("Expected textContent in modelStep") + return + } + #expect(modelText.text == "Gravity is a fundamental force.") + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func translatesToolCallsAndToolOutput() throws { + let callID = "call_abc" + let args = try GeneratedContent(json: "{\"location\":\"Paris\"}") + let toolCall = Transcript.ToolCall( + id: callID, + toolName: "get_weather", + arguments: args + ) + let transcript = Transcript( + entries: [ + .prompt( + Transcript.Prompt( + segments: [.text(Transcript.TextSegment(content: "What's the weather?"))] + ) + ), + .toolCalls(Transcript.ToolCalls(id: "calls-1", [toolCall])), + .toolOutput( + Transcript.ToolOutput( + id: callID, + toolName: "get_weather", + segments: [.text(Transcript.TextSegment(content: "Sunny, 22°C"))] + ) + ), + ] + ) + + let result = try GeminiInteractionsTranscriptTranslator.translate(transcript) + + #expect(result.steps.count == 3) + + guard case .functionCallStep(let fc) = result.steps[1] else { + Issue.record("Expected functionCallStep at index 1") + return + } + #expect(fc.id == callID) + #expect(fc.name == "get_weather") + #expect(fc.arguments?["location"] == JSONValue.string("Paris")) + + guard case .functionResultStep(let fr) = result.steps[2] else { + Issue.record("Expected functionResultStep at index 2") + return + } + #expect(fr.callId == callID) + #expect(fr.name == "get_weather") + guard case .object(let resObj) = fr.result else { + Issue.record("Expected object result") + return + } + #expect(resObj["result"] == JSONValue.string("Sunny, 22°C")) + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func translatesEmptyToolOutputToNullResult() throws { + let callID = "call_empty" + let transcript = Transcript( + entries: [ + .toolOutput( + Transcript.ToolOutput( + id: callID, + toolName: "do_something", + segments: [] + ) + ) + ] + ) + + let result = try GeminiInteractionsTranscriptTranslator.translate(transcript) + + #expect(result.steps.count == 1) + guard case .functionResultStep(let fr) = result.steps.first else { + Issue.record("Expected functionResultStep") + return + } + #expect(fr.callId == callID) + #expect(fr.name == "do_something") + #expect(fr.result == .object(["result": .null])) + } + } +#endif diff --git a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiLanguageModelInteractionsTests.swift b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiLanguageModelInteractionsTests.swift new file mode 100644 index 00000000000..98fce325703 --- /dev/null +++ b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiLanguageModelInteractionsTests.swift @@ -0,0 +1,377 @@ +// 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 canImport(FoundationModels) && compiler(>=6.4) + import Foundation + import FoundationModels + import GeminiAPIClient + import GeminiSharedDataModels + import GeminiTestUtilities + import InteractionsDataModels + import Synchronization + import Testing + + #if canImport(FoundationNetworking) + import FoundationNetworking + #endif + + @testable import GeminiLanguageModel + + extension GeminiLanguageModelTests { + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func sessionRespondInteractionsBasicText() async throws { + defer { MockHTTPURLProtocol.reset() } + let model = Self.makeMockInteractionsModel() + let expectedURL = try Self.makeExpectedInteractionsURL() + let httpResponse = try HTTPURLResponse.mock( + url: expectedURL, + headerFields: ["Content-Type": "text/event-stream"] + ) + let ssePayload = """ + event: interaction.created + data: {"id": "interaction-123", "status": "processing"} + + event: step.start + data: {"index": 0, "step": {"type": "model_output"}} + + event: step.delta + data: {"delta": {"text": "Hello, world!", "type": "text"}, "index": 0} + + event: step.stop + data: {"index": 0} + + event: interaction.completed + data: {"interaction": {"usage": {"total_input_tokens": 5, "total_output_tokens": 4}}} + + """ + let receivedRequest = Mutex(nil) + MockHTTPURLProtocol.setHandler(for: expectedURL) { request, proto in + if let body = request.httpBodyData, + let decoded = try? JSONDecoder().decode(CreateModelInteraction.self, from: body) + { + receivedRequest.withLock { $0 = decoded } + } + proto.client?.urlProtocol(proto, didReceive: httpResponse, cacheStoragePolicy: .notAllowed) + proto.client?.urlProtocol(proto, didLoad: Data(ssePayload.utf8)) + proto.client?.urlProtocolDidFinishLoading(proto) + } + + let session = LanguageModelSession(model: model) + + let response = try await session.respond(to: "Hi") + + #expect(response.content == "Hello, world!") + let captured = try #require(receivedRequest.withLock { $0 }) + #expect(captured.model?.rawValue == "gemini-3.8-flash") + #expect(captured.store == false) + #expect(captured.stream == true) + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func sessionRespondInteractionsGuidedGeneration() async throws { + defer { MockHTTPURLProtocol.reset() } + let model = Self.makeMockInteractionsModel() + let expectedURL = try Self.makeExpectedInteractionsURL() + let httpResponse = try HTTPURLResponse.mock( + url: expectedURL, + headerFields: ["Content-Type": "text/event-stream"] + ) + let ssePayload = """ + event: step.start + data: {"index": 0, "step": {"type": "model_output"}} + + event: step.delta + data: {"delta": {"text": "{\\"name\\": \\"Tokyo\\", \\"population\\": 14000000}", "type": "text"}, "index": 0} + + event: step.stop + data: {"index": 0} + + event: interaction.completed + data: {"interaction": {"usage": {"total_input_tokens": 10, "total_output_tokens": 15}}} + + """ + let receivedRequest = Mutex(nil) + MockHTTPURLProtocol.setHandler(for: expectedURL) { request, proto in + if let body = request.httpBodyData, + let decoded = try? JSONDecoder().decode(CreateModelInteraction.self, from: body) + { + receivedRequest.withLock { $0 = decoded } + } + proto.client?.urlProtocol(proto, didReceive: httpResponse, cacheStoragePolicy: .notAllowed) + proto.client?.urlProtocol(proto, didLoad: Data(ssePayload.utf8)) + proto.client?.urlProtocolDidFinishLoading(proto) + } + + let session = LanguageModelSession(model: model) + + let response = try await session.respond( + to: "Tell me about Tokyo", + generating: CitySummary.self + ) + + #expect(response.content.name == "Tokyo") + #expect(response.content.population == 14_000_000) + let captured = try #require(receivedRequest.withLock { $0 }) + let responseFormat = try #require(captured.responseFormat) + guard case .object(let schema) = responseFormat else { + Issue.record("Expected responseFormat object") + return + } + #expect(schema["type"] == .string("object")) + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func sessionRespondInteractionsToolCalling() async throws { + defer { MockHTTPURLProtocol.reset() } + let model = Self.makeMockInteractionsModel() + let expectedURL = try Self.makeExpectedInteractionsURL() + let httpResponse = try HTTPURLResponse.mock( + url: expectedURL, + headerFields: ["Content-Type": "text/event-stream"] + ) + let turn1SSE = """ + event: step.start + data: {"index": 0, "step": {"id": "call-1", "name": "get_current_time", "type": "function_call"}} + + event: step.stop + data: {"index": 0} + + """ + let turn2SSE = """ + event: step.start + data: {"index": 0, "step": {"type": "model_output"}} + + event: step.delta + data: {"delta": {"text": "The time is 12:00 PM.", "type": "text"}, "index": 0} + + event: step.stop + data: {"index": 0} + + event: interaction.completed + data: {"interaction": {"usage": {"total_input_tokens": 15, "total_output_tokens": 8}}} + + """ + let requestCount = Mutex(0) + let capturedRequests = Mutex<[CreateModelInteraction]>([]) + MockHTTPURLProtocol.setHandler(for: expectedURL) { request, proto in + let currentCount = requestCount.withLock { count -> Int in + count += 1 + return count + } + if let body = request.httpBodyData, + let decoded = try? JSONDecoder().decode(CreateModelInteraction.self, from: body) + { + capturedRequests.withLock { $0.append(decoded) } + } + proto.client?.urlProtocol(proto, didReceive: httpResponse, cacheStoragePolicy: .notAllowed) + let payload = currentCount == 1 ? turn1SSE : turn2SSE + proto.client?.urlProtocol(proto, didLoad: Data(payload.utf8)) + proto.client?.urlProtocolDidFinishLoading(proto) + } + + let session = LanguageModelSession(model: model, tools: [MockTimeTool()]) + + let response = try await session.respond(to: "What time is it?") + + #expect(response.content == "The time is 12:00 PM.") + #expect(requestCount.withLock { $0 } == 2) + let requests = capturedRequests.withLock { $0 } + #expect(requests.count == 2) + + let turn1Tools = try #require(requests.first?.tools) + guard case .function(let fn) = turn1Tools.first else { + Issue.record("Expected function tool in turn 1") + return + } + #expect(fn.name == "get_current_time") + + guard case .stepList(let turn2Steps) = try #require(requests.last?.input) else { + Issue.record("Expected stepList in turn 2") + return + } + let turn2LastStep = try #require(turn2Steps.last) + guard case .functionResultStep(let fr) = turn2LastStep else { + Issue.record("Expected functionResultStep in turn 2") + return + } + #expect(fr.name == "get_current_time") + #expect(fr.callId == "call-1") + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func sessionRespondInteractionsStreamingArgumentsToolCalling() async throws { + defer { MockHTTPURLProtocol.reset() } + let model = Self.makeMockInteractionsModel() + let expectedURL = try Self.makeExpectedInteractionsURL() + let httpResponse = try HTTPURLResponse.mock( + url: expectedURL, + headerFields: ["Content-Type": "text/event-stream"] + ) + let turn1SSE = """ + event: step.start + data: {"index": 0, "step": {"id": "call-weather", "name": "get_weather", "type": "function_call"}} + + event: step.delta + data: {"delta": {"arguments": "{\\"location\\": \\"Paris\\"}", "type": "arguments_delta"}, "index": 0} + + event: step.stop + data: {"index": 0} + + """ + let turn2SSE = """ + event: step.start + data: {"index": 0, "step": {"type": "model_output"}} + + event: step.delta + data: {"delta": {"text": "Weather in Paris is sunny.", "type": "text"}, "index": 0} + + event: step.stop + data: {"index": 0} + + event: interaction.completed + data: {"interaction": {"usage": {"total_input_tokens": 12, "total_output_tokens": 6}}} + + """ + let requestCount = Mutex(0) + MockHTTPURLProtocol.setHandler(for: expectedURL) { _, proto in + let currentCount = requestCount.withLock { count -> Int in + count += 1 + return count + } + proto.client?.urlProtocol(proto, didReceive: httpResponse, cacheStoragePolicy: .notAllowed) + let payload = currentCount == 1 ? turn1SSE : turn2SSE + proto.client?.urlProtocol(proto, didLoad: Data(payload.utf8)) + proto.client?.urlProtocolDidFinishLoading(proto) + } + + let session = LanguageModelSession(model: model, tools: [MockWeatherTool()]) + + let response = try await session.respond(to: "Weather in Paris?") + + #expect(response.content == "Weather in Paris is sunny.") + #expect(requestCount.withLock { $0 } == 2) + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func sessionStreamResponseInteractions() async throws { + defer { MockHTTPURLProtocol.reset() } + let model = Self.makeMockInteractionsModel() + let expectedURL = try Self.makeExpectedInteractionsURL() + let httpResponse = try HTTPURLResponse.mock( + url: expectedURL, + headerFields: ["Content-Type": "text/event-stream"] + ) + let ssePayload = """ + event: step.start + data: {"index": 0, "step": {"type": "model_output"}} + + event: step.delta + data: {"delta": {"text": "Hello ", "type": "text"}, "index": 0} + + event: step.delta + data: {"delta": {"text": "stream!", "type": "text"}, "index": 0} + + event: step.stop + data: {"index": 0} + + event: interaction.completed + data: {"interaction": {"usage": {"total_input_tokens": 4, "total_output_tokens": 3}}} + + """ + MockHTTPURLProtocol.setHandler(for: expectedURL) { _, proto in + proto.client?.urlProtocol(proto, didReceive: httpResponse, cacheStoragePolicy: .notAllowed) + proto.client?.urlProtocol(proto, didLoad: Data(ssePayload.utf8)) + proto.client?.urlProtocolDidFinishLoading(proto) + } + + let session = LanguageModelSession(model: model) + + let stream = session.streamResponse(to: "Hi") + var accumulated = "" + var lastUsage: LanguageModelSession.Usage? + for try await snapshot in stream { + accumulated = snapshot.content + lastUsage = snapshot.usage + } + + #expect(accumulated == "Hello stream!") + let usage = try #require(lastUsage) + #expect(usage.input.totalTokenCount == 4) + #expect(usage.output.totalTokenCount == 3) + #expect(usage.totalTokenCount == 7) + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func sessionRespondInteractionsErrorEvent() async throws { + defer { MockHTTPURLProtocol.reset() } + let model = Self.makeMockInteractionsModel() + let expectedURL = try Self.makeExpectedInteractionsURL() + let httpResponse = try HTTPURLResponse.mock( + url: expectedURL, + headerFields: ["Content-Type": "text/event-stream"] + ) + let errorSSE = """ + event: error + data: {"error": {"code": "INVALID_ARGUMENT", "message": "Interaction failed validation"}} + + """ + MockHTTPURLProtocol.setHandler(for: expectedURL) { _, proto in + proto.client?.urlProtocol(proto, didReceive: httpResponse, cacheStoragePolicy: .notAllowed) + proto.client?.urlProtocol(proto, didLoad: Data(errorSSE.utf8)) + proto.client?.urlProtocolDidFinishLoading(proto) + } + + let session = LanguageModelSession(model: model) + + do { + _ = try await session.respond(to: "Trigger error") + Issue.record("Expected error to be thrown") + } catch { + #expect("\(error)".contains("Interaction failed validation")) + } + } + + // MARK: - Helper Methods + + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + private static func makeMockInteractionsModel( + modelResource: ModelResource = .gemini38Flash, + endpointConfiguration: EndpointConfiguration = .geminiDeveloperAPI + ) -> GeminiLanguageModel { + let configuration = URLSessionConfiguration.ephemeral + configuration.protocolClasses = [MockHTTPURLProtocol.self] + return GeminiLanguageModel( + modelResource: modelResource, + endpointConfiguration: endpointConfiguration, + configuration: configuration, + apiVariant: .interactions + ) + } + + private static func makeExpectedInteractionsURL( + host: String = EndpointConfiguration.geminiDeveloperAPIHost, + apiVersion: String = EndpointConfiguration.geminiDeveloperAPIVersion + ) throws -> URL { + try #require( + URL(string: "https://\(host)/\(apiVersion)/interactions") + ) + } + } +#endif diff --git a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiLanguageModelTests.swift b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiLanguageModelTests.swift index 3e7999f7008..4d645492010 100644 --- a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiLanguageModelTests.swift +++ b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiLanguageModelTests.swift @@ -17,6 +17,7 @@ import FoundationModels import GeminiAPIClient import GeminiAPIDataModels + import GeminiSharedDataModels import GeminiTestUtilities import Synchronization import Testing diff --git a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiRequestTranslatorTests.swift b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiRequestTranslatorTests.swift index ad84820ce6b..17920eed075 100644 --- a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiRequestTranslatorTests.swift +++ b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiRequestTranslatorTests.swift @@ -16,6 +16,7 @@ import Foundation import FoundationModels import GeminiAPIDataModels + import GeminiSharedDataModels import GeminiTestUtilities import Testing diff --git a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiTranscriptTranslatorTests.swift b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiTranscriptTranslatorTests.swift index 886cd62f1fb..d0e1e85b5eb 100644 --- a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiTranscriptTranslatorTests.swift +++ b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiTranscriptTranslatorTests.swift @@ -16,6 +16,7 @@ import Foundation import FoundationModels import GeminiAPIDataModels + import GeminiSharedDataModels import GeminiTestUtilities import Testing diff --git a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GenerationSchema+GeminiTests.swift b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GenerationSchema+GeminiTests.swift index c343b7f1fe3..9fcbc804527 100644 --- a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GenerationSchema+GeminiTests.swift +++ b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GenerationSchema+GeminiTests.swift @@ -16,6 +16,7 @@ import Foundation import FoundationModels import GeminiAPIDataModels + import GeminiSharedDataModels import GeminiTestUtilities import Testing diff --git a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/BasicContentGenerationIntegrationTests.swift b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/BasicContentGenerationIntegrationTests.swift index fae81f160fc..48472f10d76 100644 --- a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/BasicContentGenerationIntegrationTests.swift +++ b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/BasicContentGenerationIntegrationTests.swift @@ -30,11 +30,15 @@ struct BasicContentGenerationIntegrationTests { @Test( .requireIntegrationTestingBackend, - arguments: IntegrationTestingBackend.availableBackends + arguments: IntegrationTestingBackend.availableBackends, + GeminiLanguageModel.APIVariant.allCases ) @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) - func sessionRespond(backend: IntegrationTestingBackend) async throws { - let model = try await backend.makeModel() + func sessionRespond( + backend: IntegrationTestingBackend, + apiVariant: GeminiLanguageModel.APIVariant + ) async throws { + let model = try await backend.makeModel(apiVariant: apiVariant) let session = LanguageModelSession(model: model) let response = try await session.respond(to: "Reply with the single word 'HELLO'.") @@ -48,11 +52,15 @@ @Test( .requireIntegrationTestingBackend, - arguments: IntegrationTestingBackend.availableBackends + arguments: IntegrationTestingBackend.availableBackends, + GeminiLanguageModel.APIVariant.allCases ) @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) - func sessionRespondMultiTurn(backend: IntegrationTestingBackend) async throws { - let model = try await backend.makeModel() + func sessionRespondMultiTurn( + backend: IntegrationTestingBackend, + apiVariant: GeminiLanguageModel.APIVariant + ) async throws { + let model = try await backend.makeModel(apiVariant: apiVariant) let session = LanguageModelSession(model: model) _ = try await session.respond(to: "My favorite color is teal.") @@ -69,11 +77,15 @@ @Test( .requireIntegrationTestingBackend, - arguments: IntegrationTestingBackend.availableBackends + arguments: IntegrationTestingBackend.availableBackends, + GeminiLanguageModel.APIVariant.allCases ) @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) - func sessionStreamResponse(backend: IntegrationTestingBackend) async throws { - let model = try await backend.makeModel() + func sessionStreamResponse( + backend: IntegrationTestingBackend, + apiVariant: GeminiLanguageModel.APIVariant + ) async throws { + let model = try await backend.makeModel(apiVariant: apiVariant) let session = LanguageModelSession(model: model) let stream = session.streamResponse( diff --git a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/GuidedGenerationIntegrationTests.swift b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/GuidedGenerationIntegrationTests.swift index 3b4b9655e12..1e8f9bbba14 100644 --- a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/GuidedGenerationIntegrationTests.swift +++ b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/GuidedGenerationIntegrationTests.swift @@ -38,11 +38,15 @@ @Test( .requireIntegrationTestingBackend, - arguments: IntegrationTestingBackend.availableBackends + arguments: IntegrationTestingBackend.availableBackends, + GeminiLanguageModel.APIVariant.allCases ) @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) - func sessionRespondWithSchema(backend: IntegrationTestingBackend) async throws { - let model = try await backend.makeModel() + func sessionRespondWithSchema( + backend: IntegrationTestingBackend, + apiVariant: GeminiLanguageModel.APIVariant + ) async throws { + let model = try await backend.makeModel(apiVariant: apiVariant) let session = LanguageModelSession(model: model) let response = try await session.respond( @@ -57,10 +61,17 @@ #expect(response.usage.output.totalTokenCount > 0) } - @Test(.requireIntegrationTestingBackend, arguments: IntegrationTestingBackend.availableBackends) + @Test( + .requireIntegrationTestingBackend, + arguments: IntegrationTestingBackend.availableBackends, + GeminiLanguageModel.APIVariant.allCases + ) @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) - func sessionStreamResponseWithSchema(backend: IntegrationTestingBackend) async throws { - let model = try await backend.makeModel() + func sessionStreamResponseWithSchema( + backend: IntegrationTestingBackend, + apiVariant: GeminiLanguageModel.APIVariant + ) async throws { + let model = try await backend.makeModel(apiVariant: apiVariant) let session = LanguageModelSession(model: model) let stream = session.streamResponse( @@ -99,10 +110,17 @@ var priority: TicketPriority } - @Test(.requireIntegrationTestingBackend, arguments: IntegrationTestingBackend.availableBackends) + @Test( + .requireIntegrationTestingBackend, + arguments: IntegrationTestingBackend.availableBackends, + GeminiLanguageModel.APIVariant.allCases + ) @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) - func sessionRespondWithEnumClassification(backend: IntegrationTestingBackend) async throws { - let model = try await backend.makeModel() + func sessionRespondWithEnumClassification( + backend: IntegrationTestingBackend, + apiVariant: GeminiLanguageModel.APIVariant + ) async throws { + let model = try await backend.makeModel(apiVariant: apiVariant) let session = LanguageModelSession(model: model) let response = try await session.respond( @@ -132,10 +150,17 @@ var subteams: [OrganizationNode] } - @Test(.requireIntegrationTestingBackend, arguments: IntegrationTestingBackend.availableBackends) + @Test( + .requireIntegrationTestingBackend, + arguments: IntegrationTestingBackend.availableBackends, + GeminiLanguageModel.APIVariant.allCases + ) @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) - func sessionRespondWithRecursiveHierarchy(backend: IntegrationTestingBackend) async throws { - let model = try await backend.makeModel() + func sessionRespondWithRecursiveHierarchy( + backend: IntegrationTestingBackend, + apiVariant: GeminiLanguageModel.APIVariant + ) async throws { + let model = try await backend.makeModel(apiVariant: apiVariant) let session = LanguageModelSession(model: model) let prompt = """ diff --git a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/IntegrationTestingBackend+GeminiLanguageModel.swift b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/IntegrationTestingBackend+GeminiLanguageModel.swift index 1a948532357..563c6d3fe57 100644 --- a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/IntegrationTestingBackend+GeminiLanguageModel.swift +++ b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/IntegrationTestingBackend+GeminiLanguageModel.swift @@ -17,23 +17,45 @@ import FoundationModels import GeminiAPIClient import GeminiTestUtilities + import Testing @testable import GeminiLanguageModel @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) extension IntegrationTestingBackend { + /// Indicates whether this backend supports the specified API variant. + /// + /// - Parameter apiVariant: The API variant to evaluate. + /// - Returns: `true` if this backend supports `apiVariant`; otherwise, `false`. + func supports(apiVariant: GeminiLanguageModel.APIVariant) -> Bool { + switch apiVariant { + case .generateContent: + return true + case .interactions: + return self == .developerAPI + } + } + /// Creates a `GeminiLanguageModel` configured for this backend. /// - /// - Parameter modelID: The model identifier to use. Defaults to `gemini-3.5-flash-lite`. + /// - Parameters: + /// - modelID: The model identifier to use. Defaults to `gemini-3.5-flash-lite`. + /// - apiVariant: The API variant to use. Defaults to `.generateContent`. /// - Returns: A configured `GeminiLanguageModel` instance. - /// - Throws: An error if model resource or credentials resolution fails. + /// - Throws: An error if model resource or credentials resolution fails, or cancels if unsupported. func makeModel( - modelID: String = ModelResource.gemini35FlashLiteID + modelID: String = ModelResource.gemini35FlashLiteID, + apiVariant: GeminiLanguageModel.APIVariant = .generateContent ) async throws -> GeminiLanguageModel { - GeminiLanguageModel( + if !supports(apiVariant: apiVariant) { + try Test.cancel("\(self) does not support the \(apiVariant) API yet.") + } + + return GeminiLanguageModel( modelResource: try modelResource(modelID: modelID), endpointConfiguration: endpointConfiguration, - headerProvider: try await makeHeaderProvider() + headerProvider: try await makeHeaderProvider(), + apiVariant: apiVariant ) } } diff --git a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/README.md b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/README.md index 3be4c1937cc..be8f0e77e7c 100644 --- a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/README.md +++ b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/README.md @@ -36,11 +36,13 @@ If credentials are not present in the environment, integration tests marked with `.requireIntegrationTestingBackend` are automatically and cleanly skipped without failing test execution. -## Supported Backends +## Supported Backends & API Variants All integration tests are parameterized across -`IntegrationTestingBackend.availableBackends`: +`IntegrationTestingBackend.availableBackends` and +`GeminiLanguageModel.APIVariant.allCases`: +### Backends * `.developerAPI`: Direct Gemini Developer API (`generativelanguage.googleapis.com`). * `.firebaseAILogicDeveloperAPI`: Firebase AI Logic proxy to Gemini Developer @@ -48,6 +50,15 @@ All integration tests are parameterized across * `.firebaseAILogicAgentPlatform(location:)`: Firebase AI Logic proxy to Gemini Enterprise Agent Platform (defaulting to `location: "global"`). +### API Variants +* `.generateContent`: Standard content generation API (`:streamGenerateContent`). +* `.interactions`: Gemini Interactions API (`/interactions`). + +> [!NOTE] +> The Interactions API is currently supported through the Developer API backend +> (`.developerAPI`). Test cases pairing `.interactions` with Firebase AI Logic +> backends are automatically cancelled until proxy support is enabled. + ## Files in this Directory * [`BasicContentGenerationIntegrationTests.swift`](BasicContentGenerationIntegrationTests.swift): @@ -64,12 +75,12 @@ All integration tests are parameterized across parameterless tools with empty arguments, and reasoning models. * [`IntegrationTestingBackend+GeminiLanguageModel.swift`](IntegrationTestingBackend+GeminiLanguageModel.swift): Convenience extension providing `backend.makeModel()` to instantiate a - pre-configured `GeminiLanguageModel`. + pre-configured `GeminiLanguageModel`, and `backend.supports(apiVariant:)`. ## Writing New Integration Tests To add a new integration test suite, parameterize on -`IntegrationTestingBackend`: +`IntegrationTestingBackend` and `GeminiLanguageModel.APIVariant`: ```swift @Suite("My New Feature Integration Tests", .requireFoundationModels) @@ -77,11 +88,15 @@ struct MyNewFeatureIntegrationTests { @Test( .tags(.integration), .requireIntegrationTestingBackend, - arguments: IntegrationTestingBackend.availableBackends + arguments: IntegrationTestingBackend.availableBackends, + GeminiLanguageModel.APIVariant.allCases ) @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) - func testFeature(backend: IntegrationTestingBackend) async throws { - let model = try await backend.makeModel() + func testFeature( + backend: IntegrationTestingBackend, + apiVariant: GeminiLanguageModel.APIVariant + ) async throws { + let model = try await backend.makeModel(apiVariant: apiVariant) let session = LanguageModelSession(model: model) // Execute test assertions... } diff --git a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/ToolCallingIntegrationTests.swift b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/ToolCallingIntegrationTests.swift index 249f75b66bf..a3a17d0114a 100644 --- a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/ToolCallingIntegrationTests.swift +++ b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/IntegrationTests/ToolCallingIntegrationTests.swift @@ -67,11 +67,15 @@ @Test( .requireIntegrationTestingBackend, - arguments: IntegrationTestingBackend.availableBackends + arguments: IntegrationTestingBackend.availableBackends, + GeminiLanguageModel.APIVariant.allCases ) @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) - func sessionRespondSingleToolCall(backend: IntegrationTestingBackend) async throws { - let model = try await backend.makeModel() + func sessionRespondSingleToolCall( + backend: IntegrationTestingBackend, + apiVariant: GeminiLanguageModel.APIVariant + ) async throws { + let model = try await backend.makeModel(apiVariant: apiVariant) let session = LanguageModelSession( model: model, tools: [WeatherTool()] @@ -101,11 +105,15 @@ @Test( .requireIntegrationTestingBackend, - arguments: IntegrationTestingBackend.availableBackends + arguments: IntegrationTestingBackend.availableBackends, + GeminiLanguageModel.APIVariant.allCases ) @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) - func sessionRespondToolWithEmptyArguments(backend: IntegrationTestingBackend) async throws { - let model = try await backend.makeModel() + func sessionRespondToolWithEmptyArguments( + backend: IntegrationTestingBackend, + apiVariant: GeminiLanguageModel.APIVariant + ) async throws { + let model = try await backend.makeModel(apiVariant: apiVariant) let session = LanguageModelSession( model: model, tools: [CurrentTimeTool()] @@ -133,11 +141,15 @@ @Test( .requireIntegrationTestingBackend, - arguments: IntegrationTestingBackend.availableBackends + arguments: IntegrationTestingBackend.availableBackends, + GeminiLanguageModel.APIVariant.allCases ) @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) - func sessionRespondSequentialToolCalls(backend: IntegrationTestingBackend) async throws { - let model = try await backend.makeModel() + func sessionRespondSequentialToolCalls( + backend: IntegrationTestingBackend, + apiVariant: GeminiLanguageModel.APIVariant + ) async throws { + let model = try await backend.makeModel(apiVariant: apiVariant) let session = LanguageModelSession( model: model, tools: [WeatherTool()] @@ -161,11 +173,15 @@ @Test( .requireIntegrationTestingBackend, - arguments: IntegrationTestingBackend.availableBackends + arguments: IntegrationTestingBackend.availableBackends, + GeminiLanguageModel.APIVariant.allCases ) @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) - func sessionRespondParallelToolCalls(backend: IntegrationTestingBackend) async throws { - let model = try await backend.makeModel() + func sessionRespondParallelToolCalls( + backend: IntegrationTestingBackend, + apiVariant: GeminiLanguageModel.APIVariant + ) async throws { + let model = try await backend.makeModel(apiVariant: apiVariant) let session = LanguageModelSession( model: model, tools: [WeatherTool()] @@ -187,13 +203,15 @@ @Test( .requireIntegrationTestingBackend, - arguments: IntegrationTestingBackend.availableBackends + arguments: IntegrationTestingBackend.availableBackends, + GeminiLanguageModel.APIVariant.allCases ) @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) func sessionRespondToolCallingModeDisallowed( - backend: IntegrationTestingBackend + backend: IntegrationTestingBackend, + apiVariant: GeminiLanguageModel.APIVariant ) async throws { - let model = try await backend.makeModel() + let model = try await backend.makeModel(apiVariant: apiVariant) let session = LanguageModelSession( model: model, tools: [WeatherTool()] @@ -214,13 +232,18 @@ @Test( .requireIntegrationTestingBackend, - arguments: IntegrationTestingBackend.availableBackends + arguments: IntegrationTestingBackend.availableBackends, + GeminiLanguageModel.APIVariant.allCases ) @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) func sessionRespondWithReasoningAndToolCalls( - backend: IntegrationTestingBackend + backend: IntegrationTestingBackend, + apiVariant: GeminiLanguageModel.APIVariant ) async throws { - let model = try await backend.makeModel(modelID: ModelResource.gemini38FlashID) + let model = try await backend.makeModel( + modelID: ModelResource.gemini38FlashID, + apiVariant: apiVariant + ) let session = LanguageModelSession( model: model, tools: [WeatherTool()] From 39e31aff49841178b60abb59c42754492b4e2e7a Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Thu, 10 Sep 2026 17:52:34 -0400 Subject: [PATCH 2/4] build(spm): sync root GeminiLanguageModel targets Synchronize the root Package.swift with the target configuration from GeminiLanguageModel/Package.swift to support the new Interactions API. * Add `GeminiSharedDataModels` and `InteractionsDataModels` targets. * Update `GeminiLanguageModel`, `GeminiAPIClient`, and `GeminiAPIDataModels` target dependencies to include the shared and interactions data model targets. * Add missing dependency on `GeminiAPIClient` and exclude `README.md` in `GeminiTestUtilities`. * Exclude `README.md` and `IntegrationTests/README.md` in `GeminiLanguageModelTests`. --- Package.swift | 46 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 740aad56843..329fa92e5d9 100644 --- a/Package.swift +++ b/Package.swift @@ -34,7 +34,7 @@ let package = Package( // MARK: - Package Manifest Builders func packageProducts() -> [Product] { - return [ + [ .library( name: "FirebaseAI", targets: [ @@ -144,7 +144,7 @@ func packageProducts() -> [Product] { } func packageDependencies() -> [Package.Dependency] { - return [ + [ .package( url: "https://github.com/google/promises.git", "2.4.0" ..< "3.0.0" @@ -1720,6 +1720,8 @@ func firebaseAILogicDependencies() -> [Target.Dependency] { dependencies: [ "GeminiAPIClient", "GeminiAPIDataModels", + "InteractionsDataModels", + "GeminiSharedDataModels", ], path: "GeminiLanguageModel/Sources/GeminiLanguageModel", swiftSettings: swiftSettings @@ -1731,12 +1733,18 @@ func firebaseAILogicDependencies() -> [Target.Dependency] { "GeminiTestUtilities", ], path: "GeminiLanguageModel/Tests/GeminiLanguageModelTests", + exclude: [ + "IntegrationTests/README.md", + "README.md", + ], swiftSettings: swiftSettings ), .target( name: "GeminiAPIClient", dependencies: [ "GeminiAPIDataModels", + "InteractionsDataModels", + "GeminiSharedDataModels", ], path: "GeminiLanguageModel/Sources/GeminiAPIClient", swiftSettings: swiftSettings @@ -1750,8 +1758,20 @@ func firebaseAILogicDependencies() -> [Target.Dependency] { path: "GeminiLanguageModel/Tests/GeminiAPIClientTests", swiftSettings: swiftSettings ), + .target( + name: "GeminiSharedDataModels", + path: "GeminiLanguageModel/Sources/GeminiSharedDataModels", + swiftSettings: [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("MemberImportVisibility"), + .swiftLanguageMode(.v6), + ] + ), .target( name: "GeminiAPIDataModels", + dependencies: [ + "GeminiSharedDataModels", + ], path: "GeminiLanguageModel/Sources/GeminiAPIDataModels", swiftSettings: [ .enableUpcomingFeature("ExistentialAny"), @@ -1759,17 +1779,35 @@ func firebaseAILogicDependencies() -> [Target.Dependency] { .swiftLanguageMode(.v6), ] ), + .target( + name: "InteractionsDataModels", + dependencies: [ + "GeminiSharedDataModels", + ], + path: "GeminiLanguageModel/Sources/InteractionsDataModels", + swiftSettings: [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("MemberImportVisibility"), + .swiftLanguageMode(.v6), + ] + ), .target( name: "GeminiTestUtilities", + dependencies: [ + "GeminiAPIClient", + ], path: "GeminiLanguageModel/Tests/GeminiTestUtilities", - swiftSettings: swiftSettings, + exclude: [ + "README.md", + ], + swiftSettings: swiftSettings ), ] } #endif // compiler(>=6.4) && canImport(FoundationModels) func isFoundationModelsSupportedPlatformSwiftSetting() -> SwiftSetting { - return SwiftSetting.define( + SwiftSetting.define( "IS_FOUNDATION_MODELS_SUPPORTED_PLATFORM", .when(platforms: [.iOS, .macCatalyst, .macOS, .visionOS]) ) From 0906e7b527c63c74df8509dd4e7c1f52699e59eb Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Thu, 10 Sep 2026 18:51:12 -0400 Subject: [PATCH 3/4] feat(ai): add gfm CLI and developer auth trait Add the `gfm` CLI sample tool along with package trait support for direct Gemini Developer API authentication via environment variables. * Add `gfm` CLI tool in `GeminiLanguageModel/Samples/gfm` supporting content generation, streaming, multi-turn chat, model availability evaluations, and JSON schema inspection. * Add `GeminiDeveloperAPIEnvironmentAuth` package trait to conditionally expose standalone `GeminiLanguageModel` initialization. * Implement environment API key resolution in `GeminiLanguageModel`, preferring `GOOGLE_API_KEY` over `GEMINI_API_KEY`. * Add `GeminiLanguageModel.Error.missingAPIKey` when required credentials are not found in the environment. * Expose standard `EndpointConfiguration.geminiDeveloperAPI` static endpoint property. * Add unit test coverage for trait-gated initializers and key resolution precedence without mutating process environment state. --- GeminiLanguageModel/Package.swift | 10 + GeminiLanguageModel/Samples/gfm/Package.swift | 69 ++++ GeminiLanguageModel/Samples/gfm/README.md | 226 +++++++++++++ .../Samples/gfm/Sources/GFM/main.swift | 46 +++ .../GFMCore/Commands/AvailableCommand.swift | 82 +++++ .../GFMCore/Commands/ChatCommand.swift | 222 +++++++++++++ .../GFMCore/Commands/RespondCommand.swift | 248 ++++++++++++++ .../GFMCore/Commands/SchemaCommand.swift | 91 ++++++ .../gfm/Sources/GFMCore/GFMCommand.swift | 39 +++ .../GFMCore/Models/ModelSelection.swift | 158 +++++++++ .../GFMCore/Schema/SchemaBuilder.swift | 303 ++++++++++++++++++ .../GFMCore/Sessions/SessionManager.swift | 133 ++++++++ .../GFMCore/Tools/CurrentTimeTool.swift | 50 +++ .../GFMTests/AvailableCommandTests.swift | 75 +++++ .../Tests/GFMTests/CurrentTimeToolTests.swift | 45 +++ .../GFMTests/RespondAndChatCommandTests.swift | 120 +++++++ .../Tests/GFMTests/SchemaCommandTests.swift | 80 +++++ .../Tests/GFMTests/SessionManagerTests.swift | 64 ++++ .../EndpointConfiguration.swift | 10 + .../GeminiAPIClient/GeminiAPIClient.swift | 2 +- .../GeminiLanguageModel+Error.swift | 18 ++ .../GeminiLanguageModel.swift | 78 ++++- .../GeminiLanguageModelTests.swift | 66 ++++ .../GeminiTestUtilities/TestConstants.swift | 6 - 24 files changed, 2233 insertions(+), 8 deletions(-) create mode 100644 GeminiLanguageModel/Samples/gfm/Package.swift create mode 100644 GeminiLanguageModel/Samples/gfm/README.md create mode 100644 GeminiLanguageModel/Samples/gfm/Sources/GFM/main.swift create mode 100644 GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/AvailableCommand.swift create mode 100644 GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/ChatCommand.swift create mode 100644 GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/RespondCommand.swift create mode 100644 GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/SchemaCommand.swift create mode 100644 GeminiLanguageModel/Samples/gfm/Sources/GFMCore/GFMCommand.swift create mode 100644 GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Models/ModelSelection.swift create mode 100644 GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Schema/SchemaBuilder.swift create mode 100644 GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Sessions/SessionManager.swift create mode 100644 GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Tools/CurrentTimeTool.swift create mode 100644 GeminiLanguageModel/Samples/gfm/Tests/GFMTests/AvailableCommandTests.swift create mode 100644 GeminiLanguageModel/Samples/gfm/Tests/GFMTests/CurrentTimeToolTests.swift create mode 100644 GeminiLanguageModel/Samples/gfm/Tests/GFMTests/RespondAndChatCommandTests.swift create mode 100644 GeminiLanguageModel/Samples/gfm/Tests/GFMTests/SchemaCommandTests.swift create mode 100644 GeminiLanguageModel/Samples/gfm/Tests/GFMTests/SessionManagerTests.swift diff --git a/GeminiLanguageModel/Package.swift b/GeminiLanguageModel/Package.swift index 5bd2f441d40..9dcce576aca 100644 --- a/GeminiLanguageModel/Package.swift +++ b/GeminiLanguageModel/Package.swift @@ -31,6 +31,16 @@ let package = Package( targets: ["GeminiLanguageModel"] ) ], + traits: [ + .trait( + name: "GeminiDeveloperAPIEnvironmentAuth", + description: + """ + Enables public initialization with the Gemini Developer API using the GOOGLE_API_KEY or \ + GEMINI_API_KEY environment variable. + """ + ) + ], targets: [ .target( name: "GeminiLanguageModel", diff --git a/GeminiLanguageModel/Samples/gfm/Package.swift b/GeminiLanguageModel/Samples/gfm/Package.swift new file mode 100644 index 00000000000..4d3d6dd95da --- /dev/null +++ b/GeminiLanguageModel/Samples/gfm/Package.swift @@ -0,0 +1,69 @@ +// swift-tools-version: 6.1 + +// 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. + +import PackageDescription + +let defaultSwiftSettings: [SwiftSetting] = [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault"), + .enableUpcomingFeature("MemberImportVisibility"), +] + +let package = Package( + name: "gfm", + platforms: [.macOS(.v15)], + products: [ + .executable( + name: "gfm", + targets: ["GFM"] + ), + .library( + name: "GFMCore", + targets: ["GFMCore"] + ), + ], + dependencies: [ + .package(path: "../../", traits: ["GeminiDeveloperAPIEnvironmentAuth"]), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.8.2"), + ], + targets: [ + .executableTarget( + name: "GFM", + dependencies: [ + "GFMCore", + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + swiftSettings: defaultSwiftSettings + ), + .target( + name: "GFMCore", + dependencies: [ + .product(name: "GeminiLanguageModel", package: "GeminiLanguageModel"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + swiftSettings: defaultSwiftSettings + ), + .testTarget( + name: "GFMTests", + dependencies: [ + "GFMCore", + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + swiftSettings: defaultSwiftSettings + ), + ], + swiftLanguageModes: [.v6] +) diff --git a/GeminiLanguageModel/Samples/gfm/README.md b/GeminiLanguageModel/Samples/gfm/README.md new file mode 100644 index 00000000000..b13dde090c8 --- /dev/null +++ b/GeminiLanguageModel/Samples/gfm/README.md @@ -0,0 +1,226 @@ +# gfm: Gemini Foundation Models CLI + +`gfm` is a command-line utility modeled after Apple's built-in `fm` (Foundation +Models) CLI. It provides an interface to generate content, run interactive chat +sessions, validate model availability, and construct structured schemas using +Google Gemini (`GeminiLanguageModel`) and Apple System models +(`SystemLanguageModel`). + +## Requirements + +* macOS 26.0 or later (macOS 27.0+ recommended for `SystemLanguageModel`) +* Swift 6.0 or later / Xcode 26.2+ +* A Gemini API key (obtainable from Google AI Studio) for `gemini` models + +## Authentication and Model Selection + +`gfm` defaults to using the `gemini` model family (`gemini-3.5-flash-lite`). + +### Setting your API key + +Authenticate with the Gemini Developer API by exporting an environment variable +in your shell: + +```bash +export GOOGLE_API_KEY="your-gemini-api-key" +# or +export GEMINI_API_KEY="your-gemini-api-key" +``` + +> [!NOTE] +> If both environment variables are set, `GOOGLE_API_KEY` takes precedence over +> `GEMINI_API_KEY`. + +Alternatively, pass your key explicitly using the `--api-key` flag: + +```bash +gfm respond --api-key "your-gemini-api-key" "Explain quantum computing briefly." +``` + +### Model selection options + +* `-m, --model `: Select the backend model (`gemini` by default). +* `--gemini-model `: Specify a Gemini model variant (defaults to + `gemini-3.5-flash-lite`, e.g. `gemini-2.5-pro` or `gemini-2.5-flash`). + +## Building and Running + +### 1. Run directly with `swift run` + +You can run `gfm` without installing it using `swift run`. + +#### From the repository root: + +```bash +# General help +swift run --package-path GeminiLanguageModel/Samples/gfm gfm --help + +# Check model availability +swift run --package-path GeminiLanguageModel/Samples/gfm gfm available + +# Generate a response +swift run --package-path GeminiLanguageModel/Samples/gfm gfm respond "Explain Swift concurrency." + +# Start an interactive chat session +swift run --package-path GeminiLanguageModel/Samples/gfm gfm chat +``` + +#### From the sample directory: + +```bash +cd GeminiLanguageModel/Samples/gfm + +# Run commands directly +swift run gfm available +swift run gfm respond "Write a haiku about Xcode." +swift run gfm chat +``` + +### 2. Build and run the compiled binary + +For repeated executions without SwiftPM dependency resolution overhead, compile +a release executable: + +```bash +# From the repository root +swift build --package-path GeminiLanguageModel/Samples/gfm -c release + +# Run the compiled binary +./GeminiLanguageModel/Samples/gfm/.build/release/gfm --version +./GeminiLanguageModel/Samples/gfm/.build/release/gfm available +``` + +#### Optional: Create an alias or link to your `$PATH` + +```bash +# Shell alias +alias gfm="$(pwd)/GeminiLanguageModel/Samples/gfm/.build/release/gfm" + +# Or symlink into local binaries +ln -sf "$(pwd)/GeminiLanguageModel/Samples/gfm/.build/release/gfm" /usr/local/bin/gfm +``` + +## Commands and Usage + +### 1. `available` + +Check the availability of configured language models on your system. + +```bash +# Check all models +gfm available + +# Check Gemini model availability +gfm available -m gemini + +# Check Apple System Language Model availability +gfm available -m system +``` + +### 2. `respond` + +Generate a response for a single prompt or piped input with real-time streaming. + +```bash +# Direct argument prompt +gfm respond "Write a haiku about Swift concurrency." + +# Read prompt from piped stdin +cat document.txt | gfm respond + +# Pipe text and ask a question about it +cat article.txt | gfm respond "What are the main takeaways?" +git diff | gfm respond "Summarize these git changes as a commit message." + +# System instructions +gfm respond -i "You are an expert iOS engineer." "How does @Observable work?" + +# Use Apple's on-device System model +gfm respond -m system "Why is the sky blue?" + +# Save response transcript to a session +gfm respond --save-transcript "my-session" "Remember that my favorite color is blue." + +# Resume an existing session +gfm respond --resume "my-session" "What is my favorite color?" + +# Call built-in demo tools +gfm respond --tool current-time "What time is it right now?" +``` + +#### Structured Output with `--schema` + +You can enforce structured JSON outputs by pairing `--schema` with property +definitions: + +```bash +gfm respond \ + --schema 'name:string age:integer role:string' \ + "Generate a fictional software engineer profile." +``` + +### 3. `chat` + +Start an interactive, multi-turn chat session with streaming responses. + +```bash +# Start an interactive chat using the default Gemini model +gfm chat + +# Start a chat with the on-device System model +gfm chat -m system + +# Start a chat with system instructions +gfm chat -i "You are a helpful coding assistant." + +# Enable tool calling in chat +gfm chat --tool current-time + +# Resume a previous session +gfm chat -r "my-session" + +# Continue the most recent session +gfm chat -c +``` + +#### In-Chat Slash Commands + +While in a chat session, type any of the following commands: + +* `/help` - Display available chat commands. +* `/clear` - Clear the current conversation transcript. +* `/sessions` - List saved sessions stored in `~/.fm/sessions/`. +* `/exit` - Save transcript and quit the session. + +### 4. `schema object` + +Generate and preview standard JSON schema definitions: + +```bash +# Primitive types +gfm schema object --name Person --string name --integer age --boolean is_active + +# Optional and array modifiers +gfm schema object --name Dog --string breed --description "Breed of dog" --boolean friendly --optional --string tags --array + +# Nested object properties using dot notation +gfm schema object --name Restaurant --string name --string address.street --string address.zip +``` + +## Running Tests + +Run the test suite from the repository root: + +```bash +swift test --package-path GeminiLanguageModel/Samples/gfm +``` + +Or from the sample directory: + +```bash +cd GeminiLanguageModel/Samples/gfm +swift test +``` + +All unit tests run deterministically without requiring an active network +connection or valid API credentials. diff --git a/GeminiLanguageModel/Samples/gfm/Sources/GFM/main.swift b/GeminiLanguageModel/Samples/gfm/Sources/GFM/main.swift new file mode 100644 index 00000000000..200ea9a0016 --- /dev/null +++ b/GeminiLanguageModel/Samples/gfm/Sources/GFM/main.swift @@ -0,0 +1,46 @@ +// 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. + +import ArgumentParser +import Darwin +import Foundation +import GFMCore + +if #available(macOS 27.0, *) { + do { + let command = try await GFMCommand.asyncParseAsRoot() + try await execute(command) + } catch { + GFMCommand.exit(withError: error) + } +} else { + fputs("Error: gfm requires macOS 27.0 or newer.\n", stderr) + exit(1) +} + +@available(macOS 27.0, *) +private func execute(_ command: any ParsableCommand) async throws { + if let asyncCommand = command as? any AsyncParsableCommand { + try await executeAsync(asyncCommand) + } else { + var syncCommand = command + try syncCommand.run() + } +} + +@available(macOS 27.0, *) +private func executeAsync(_ command: C) async throws { + var asyncCommand = command + try await asyncCommand.run() +} diff --git a/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/AvailableCommand.swift b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/AvailableCommand.swift new file mode 100644 index 00000000000..9c11c14ee76 --- /dev/null +++ b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/AvailableCommand.swift @@ -0,0 +1,82 @@ +// 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. + +public import ArgumentParser +import Foundation + +#if canImport(FoundationModels) && compiler(>=6.4) + import FoundationModels + + /// Command that checks and prints model availability. + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + @available(tvOS, unavailable) + public struct AvailableCommand: AsyncParsableCommand { + public static let configuration = CommandConfiguration( + commandName: "available", + abstract: "Check model availability." + ) + + @Option( + name: [.short, .long], + help: "Model to check (gemini, system); checks all if omitted." + ) + public var model: ModelChoice? + + @Option( + name: .long, + help: "Gemini model ID to check." + ) + public var geminiModel: String = ModelResolver.defaultGeminiModelID + + @Option( + name: .long, + help: "Explicit Gemini API key." + ) + public var apiKey: String? + + public init() {} + + public func run() async throws { + let resolver = ModelResolver( + choice: model ?? .gemini, + geminiModelID: geminiModel, + explicitAPIKey: apiKey + ) + + let modelsToCheck: [ModelChoice] + if let model { + modelsToCheck = [model] + } else { + modelsToCheck = [.system, .gemini] + } + + var allAvailable = true + + for choice in modelsToCheck { + let status = resolver.checkAvailability(for: choice) + switch status { + case .available: + print("\(choice.displayName) available") + case .unavailable(let reason): + allAvailable = false + print("\(choice.displayName) unavailable: \(reason)") + } + } + + if !allAvailable { + throw ExitCode.failure + } + } + } +#endif diff --git a/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/ChatCommand.swift b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/ChatCommand.swift new file mode 100644 index 00000000000..f6db4496848 --- /dev/null +++ b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/ChatCommand.swift @@ -0,0 +1,222 @@ +// 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. + +public import ArgumentParser +import Foundation + +#if canImport(FoundationModels) && compiler(>=6.4) + import FoundationModels + + /// Command that starts an interactive multi-turn chat session. + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + @available(tvOS, unavailable) + public struct ChatCommand: AsyncParsableCommand { + public static let configuration = CommandConfiguration( + commandName: "chat", + abstract: "Start an interactive chat session." + ) + + @Option( + name: [.short, .long], + help: "Model to use (gemini, system; default: gemini)." + ) + public var model: ModelChoice = .gemini + + @Option( + name: .long, + help: "Gemini model ID to use when targetting Gemini." + ) + public var geminiModel: String = ModelResolver.defaultGeminiModelID + + @Option( + name: .long, + help: "Explicit Gemini API key." + ) + public var apiKey: String? + + @Option( + name: [.short, .long], + help: "Instructions for the model." + ) + public var instructions: String? + + @Option( + name: [.short, .customLong("resume")], + help: "Resume a saved chat session." + ) + public var resume: String? + + @Flag( + name: .long, + help: "Continue the most recent chat session." + ) + public var `continue`: Bool = false + + @Option( + name: .long, + help: "Built-in tool to enable (current-time; repeatable)." + ) + public var tool: [String] = [] + + public init() {} + + public func run() async throws { + let resolver = ModelResolver( + choice: model, + geminiModelID: geminiModel, + explicitAPIKey: apiKey + ) + let languageModel = try resolver.makeModel() + + var tools: [any Tool] = [] + for toolName in tool { + if toolName == "current-time" || toolName == "current_time" { + tools.append(CurrentTimeTool()) + } else { + throw ValidationError("Unsupported tool '\(toolName)'. Available tools: current-time") + } + } + + let sessionManager = SessionManager() + let sessionName: String + var session: LanguageModelSession + + if `continue` { + guard let recentName = try sessionManager.mostRecentSessionName() else { + throw ValidationError("No saved sessions found in ~/.fm/sessions/") + } + sessionName = recentName + let transcript = try sessionManager.loadSession(nameOrPath: sessionName) + session = LanguageModelSession(model: languageModel, tools: tools, transcript: transcript) + print("Resumed session '\(sessionName)'.") + } else if let resume { + sessionName = resume + let transcript = try sessionManager.loadSession(nameOrPath: sessionName) + session = LanguageModelSession(model: languageModel, tools: tools, transcript: transcript) + print("Resumed session '\(sessionName)'.") + } else { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "yyyyMMdd-HHmmss" + sessionName = "session-\(formatter.string(from: Date()))" + session = LanguageModelSession( + model: languageModel, + tools: tools, + instructions: instructions + ) + } + + print("Session: \(sessionName). Type /help for available commands.\n") + + while true { + print("> ", terminator: "") + fflush(stdout) + + guard let line = readLine() else { + print("\nExiting chat session.") + break + } + + let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty { + continue + } + + if trimmed.hasPrefix("/") { + let shouldExit = try handleSlashCommand( + trimmed, + sessionName: sessionName, + sessionManager: sessionManager, + session: &session, + languageModel: languageModel, + tools: tools + ) + if shouldExit { + break + } + continue + } + + let responseStream = session.streamResponse(to: trimmed) + var printed = "" + for try await snapshot in responseStream { + let current = snapshot.content + if current.count > printed.count { + let diff = current.dropFirst(printed.count) + print(diff, terminator: "") + fflush(stdout) + printed = current + } + } + print("\n") + + try? sessionManager.saveSession(transcript: session.transcript, nameOrPath: sessionName) + } + } + + private func handleSlashCommand( + _ command: String, + sessionName: String, + sessionManager: SessionManager, + session: inout LanguageModelSession, + languageModel: any LanguageModel, + tools: [any Tool] + ) throws -> Bool { + let parts = command.split(separator: " ", maxSplits: 1).map(String.init) + let cmd = parts.first ?? command + + switch cmd { + case "/help": + print("Available commands:") + print(" /exit, /quit Save and exit the current chat session") + print(" /clear Clear session history and reset context") + print(" /sessions List all saved sessions in ~/.fm/sessions/") + print(" /help Show this help message\n") + return false + + case "/exit", "/quit": + try? sessionManager.saveSession(transcript: session.transcript, nameOrPath: sessionName) + print("Saved session to ~/.fm/sessions/\(sessionName).json. Goodbye!") + return true + + case "/clear": + session = LanguageModelSession( + model: languageModel, + tools: tools, + instructions: instructions + ) + try? sessionManager.saveSession(transcript: session.transcript, nameOrPath: sessionName) + print("Session history cleared.\n") + return false + + case "/sessions": + let sessions = try sessionManager.listSessionNames() + if sessions.isEmpty { + print("No saved sessions found.\n") + } else { + print("Saved sessions:") + for s in sessions { + print(" \(s)") + } + print() + } + return false + + default: + print("Unknown command '\(cmd)'. Type /help for available commands.\n") + return false + } + } + } +#endif diff --git a/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/RespondCommand.swift b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/RespondCommand.swift new file mode 100644 index 00000000000..dc62da4988b --- /dev/null +++ b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/RespondCommand.swift @@ -0,0 +1,248 @@ +// 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. + +public import ArgumentParser +import Foundation + +import var Darwin.C.STDIN_FILENO +import func Darwin.C.isatty + +#if canImport(FoundationModels) && compiler(>=6.4) + import FoundationModels + + /// Command that generates a response from the selected model for a given prompt. + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + @available(tvOS, unavailable) + public struct RespondCommand: AsyncParsableCommand { + public static let configuration = CommandConfiguration( + commandName: "respond", + abstract: "Generate a response to a prompt." + ) + + @Argument( + help: "Prompt for the model (reads from stdin if omitted, or combines with stdin when piped)." + ) + public var prompt: String? + + @Option( + name: [.short, .long], + help: "Model to use (gemini, system; default: gemini)." + ) + public var model: ModelChoice = .gemini + + @Option( + name: .long, + help: "Gemini model ID to use when targetting Gemini." + ) + public var geminiModel: String = ModelResolver.defaultGeminiModelID + + @Option( + name: .long, + help: "Explicit Gemini API key." + ) + public var apiKey: String? + + @Option( + name: [.short, .long], + help: "Instructions for the model to follow." + ) + public var instructions: String? + + @Option( + name: .long, + help: "Path to a structured output schema file." + ) + public var schema: String? + + @Option( + name: .long, + help: "Text segment to include in the prompt (repeatable)." + ) + public var text: [String] = [] + + @Option( + name: .long, + help: "Built-in tool to enable (current-time; repeatable)." + ) + public var tool: [String] = [] + + @Option( + name: .long, + help: "Continue a saved conversation from its transcript." + ) + public var resume: String? + + @Option( + name: .long, + help: "Save transcript to a file after responding." + ) + public var saveTranscript: String? + + @Flag( + inversion: .prefixedNo, + help: "Stream the output as it's generated (default: on)." + ) + public var stream: Bool = true + + @Flag( + name: [.short, .long], + help: "Use greedy sampling." + ) + public var greedy: Bool = false + + @Flag( + name: [.short, .long], + help: "Print verbose output." + ) + public var verbose: Bool = false + + public init() {} + + public func run() async throws { + let resolvedPrompt = try resolvePrompt() + + let resolver = ModelResolver( + choice: model, + geminiModelID: geminiModel, + explicitAPIKey: apiKey + ) + let languageModel = try resolver.makeModel() + + var tools: [any Tool] = [] + for toolName in tool { + if toolName == "current-time" || toolName == "current_time" { + tools.append(CurrentTimeTool()) + } else { + throw ValidationError("Unsupported tool '\(toolName)'. Available tools: current-time") + } + } + + let sessionManager = SessionManager() + let session: LanguageModelSession + + if let resume { + let transcript = try sessionManager.loadSession(nameOrPath: resume) + if instructions != nil { + throw ValidationError( + "Cannot specify both a saved transcript and --instructions. The transcript already contains its instructions." + ) + } + session = LanguageModelSession(model: languageModel, tools: tools, transcript: transcript) + } else { + session = LanguageModelSession( + model: languageModel, + tools: tools, + instructions: instructions + ) + } + + var options = GenerationOptions() + if greedy { + options.temperature = 0.0 + } + + if let schemaPath = schema { + let schemaURL = URL(fileURLWithPath: NSString(string: schemaPath).expandingTildeInPath) + let schemaData = try Data(contentsOf: schemaURL) + let genSchema = try JSONDecoder().decode(GenerationSchema.self, from: schemaData) + + if stream { + let responseStream = session.streamResponse( + to: resolvedPrompt, + schema: genSchema, + options: options + ) + var printed = "" + for try await snapshot in responseStream { + let current = snapshot.content.jsonString + if current.count > printed.count { + let diff = current.dropFirst(printed.count) + print(diff, terminator: "") + fflush(stdout) + printed = current + } + } + print() + } else { + let response = try await session.respond( + to: resolvedPrompt, + schema: genSchema, + options: options + ) + print(response.content.jsonString) + } + } else { + if stream { + let responseStream = session.streamResponse( + to: resolvedPrompt, + options: options + ) + var printed = "" + for try await snapshot in responseStream { + let current = snapshot.content + if current.count > printed.count { + let diff = current.dropFirst(printed.count) + print(diff, terminator: "") + fflush(stdout) + printed = current + } + } + print() + } else { + let response = try await session.respond( + to: resolvedPrompt, + options: options + ) + print(response.content) + } + } + + if let saveTranscript { + try sessionManager.saveSession(transcript: session.transcript, nameOrPath: saveTranscript) + } + } + + func resolvePrompt( + stdinReader: () throws -> String? = { + guard isatty(STDIN_FILENO) == 0 else { return nil } + guard let stdinData = try? FileHandle.standardInput.readToEnd() else { return nil } + guard let text = String(data: stdinData, encoding: .utf8) else { + throw ValidationError( + "Standard input was not valid UTF-8. Pipe UTF-8 encoded text only." + ) + } + let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed + } + ) throws -> String { + var segments: [String] = [] + if let prompt, !prompt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + segments.append(prompt) + } + segments.append(contentsOf: text) + + let stdinContent = try stdinReader() + + if let stdinContent, !segments.isEmpty { + return stdinContent + "\n\n" + segments.joined(separator: "\n") + } else if let stdinContent { + return stdinContent + } else if !segments.isEmpty { + return segments.joined(separator: "\n") + } + + throw ValidationError("Prompt must be provided as an argument or via standard input.") + } + } +#endif diff --git a/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/SchemaCommand.swift b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/SchemaCommand.swift new file mode 100644 index 00000000000..faefee9bedb --- /dev/null +++ b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/SchemaCommand.swift @@ -0,0 +1,91 @@ +// 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. + +public import ArgumentParser +import Foundation + +#if canImport(FoundationModels) && compiler(>=6.4) + import FoundationModels + + /// Command for generating structured output schemas. + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + @available(tvOS, unavailable) + public struct SchemaCommand: ParsableCommand { + public static let configuration = CommandConfiguration( + commandName: "schema", + abstract: "Generate a structured output generation schema.", + subcommands: [ObjectSchemaCommand.self] + ) + + public init() {} + } + + /// Subcommand for generating a structured output schema for an object type. + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + @available(tvOS, unavailable) + public struct ObjectSchemaCommand: ParsableCommand { + public static let configuration = CommandConfiguration( + commandName: "object", + abstract: "Generate a structured output schema for an object type.", + discussion: """ + PROPERTY DECLARATIONS + --anyOf Build an anyOf union; subsequent --schema args become choices + --boolean Declare a boolean property + --double Declare a floating-point property + --integer Declare an integer property + --object Declare a nested object property (follow with --schema) + --schema Provide a structured output schema (after --object or --anyOf) + --string Declare a string property + + PROPERTY MODIFIERS + --array Mark the preceding property as an array of that type + --description Set a description on the preceding property + --optional Mark the preceding property as optional + + Use dot notation in property names (e.g. 'address.street') to create nested objects. + """ + ) + + @Argument( + parsing: .allUnrecognized, + help: "Property declarations, nested types, and modifiers." + ) + public var rawArguments: [String] = [] + + public init() {} + + public init(rawArguments: [String]) { + self.rawArguments = rawArguments + } + + public func run() throws { + let argsToParse: [String] + if !rawArguments.isEmpty { + argsToParse = rawArguments + } else { + // Find "object" in CommandLine.arguments and take everything after it + let cliArgs = CommandLine.arguments + if let objIndex = cliArgs.firstIndex(of: "object") { + argsToParse = Array(cliArgs.dropFirst(objIndex + 1)) + } else { + argsToParse = [] + } + } + + let schema = try SchemaBuilder.build(from: argsToParse) + let jsonString = try SchemaBuilder.formatJSON(schema) + print(jsonString) + } + } +#endif diff --git a/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/GFMCommand.swift b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/GFMCommand.swift new file mode 100644 index 00000000000..736a2d179d0 --- /dev/null +++ b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/GFMCommand.swift @@ -0,0 +1,39 @@ +// 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. + +public import ArgumentParser +import Foundation + +#if canImport(FoundationModels) && compiler(>=6.4) + import FoundationModels + + /// The root command for the `gfm` command-line utility. + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + @available(tvOS, unavailable) + public struct GFMCommand: AsyncParsableCommand { + public static let configuration = CommandConfiguration( + commandName: "gfm", + abstract: "Gemini and Apple Foundation Models CLI", + version: "1.0.0", + subcommands: [ + AvailableCommand.self, + ChatCommand.self, + RespondCommand.self, + SchemaCommand.self, + ] + ) + + public init() {} + } +#endif diff --git a/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Models/ModelSelection.swift b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Models/ModelSelection.swift new file mode 100644 index 00000000000..4a6bfd252de --- /dev/null +++ b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Models/ModelSelection.swift @@ -0,0 +1,158 @@ +// 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. + +public import ArgumentParser +import Foundation + +#if canImport(FoundationModels) && compiler(>=6.4) + public import FoundationModels + public import GeminiLanguageModel + + /// Supported model families for the `gfm` command-line utility. + public enum ModelChoice: String, ExpressibleByArgument, CaseIterable, Sendable { + case gemini + case system + + /// Short human-readable display name for this model choice. + public var displayName: String { + switch self { + case .gemini: + return "Gemini model" + case .system: + return "System model" + } + } + } + + /// Availability status for a model evaluated by `gfm`. + public enum ModelAvailabilityStatus: Sendable, Equatable { + case available + case unavailable(reason: String) + + public var isAvailable: Bool { + if case .available = self { + return true + } + return false + } + } + + /// Helper for configuring and resolving language models from CLI options and environment. + public struct ModelResolver: Sendable { + /// Default Gemini model ID. + public static let defaultGeminiModelID = "gemini-3.5-flash-lite" + + /// The selected model family. Defaults to `.gemini`. + public var choice: ModelChoice + + /// Custom Gemini model ID override. + public var geminiModelID: String + + /// Optional explicit API key passed via `--api-key`. + public var explicitAPIKey: String? + + /// Initializes a new model resolver with the given options. + /// + /// - Parameters: + /// - choice: The model family to use. Defaults to `.gemini`. + /// - geminiModelID: The model identifier to use when targetting Gemini. Defaults to + /// `"gemini-3.5-flash-lite"`. + /// - explicitAPIKey: An optional explicit API key. + public init( + choice: ModelChoice = .gemini, + geminiModelID: String = defaultGeminiModelID, + explicitAPIKey: String? = nil + ) { + self.choice = choice + self.geminiModelID = geminiModelID + self.explicitAPIKey = explicitAPIKey + } + + /// Resolves the effective Gemini API key from explicit options or environment variables. + /// + /// - Returns: The resolved API key, or `nil` if not configured. + public func resolveAPIKey() -> String? { + if let explicitAPIKey, !explicitAPIKey.isEmpty { + return explicitAPIKey + } + let env = ProcessInfo.processInfo.environment + if let envKey = env["GEMINI_API_KEY"], !envKey.isEmpty { + return envKey + } + if let googleKey = env["GOOGLE_API_KEY"], !googleKey.isEmpty { + return googleKey + } + return nil + } + + /// Checks the availability of the specified model choice. + /// + /// - Parameter targetChoice: The model choice to check. + /// - Returns: The availability status. + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + @available(tvOS, unavailable) + public func checkAvailability(for targetChoice: ModelChoice) -> ModelAvailabilityStatus { + switch targetChoice { + case .gemini: + if resolveAPIKey() != nil { + return .available + } else { + return .unavailable(reason: "missingAPIKey") + } + + case .system: + let availability = SystemLanguageModel.default.availability + switch availability { + case .available: + return .available + case .unavailable(let reason): + switch reason { + case .deviceNotEligible: + return .unavailable(reason: "deviceNotEligible") + case .appleIntelligenceNotEnabled: + return .unavailable(reason: "appleIntelligenceNotEnabled") + case .modelNotReady: + return .unavailable(reason: "modelNotReady") + @unknown default: + return .unavailable(reason: "unknownReason") + } + } + } + } + + /// Instantiates the configured `LanguageModel` instance. + /// + /// - Returns: An instance conforming to `LanguageModel`. + /// - Throws: `ValidationError` if the model cannot be initialized (e.g. missing API key). + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + @available(tvOS, unavailable) + public func makeModel() throws -> any LanguageModel { + switch choice { + case .gemini: + guard let apiKey = resolveAPIKey() else { + throw ValidationError( + "Gemini API key is required. Provide --api-key or set GEMINI_API_KEY (or GOOGLE_API_KEY)." + ) + } + if let explicitAPIKey, !explicitAPIKey.isEmpty { + setenv("GEMINI_API_KEY", explicitAPIKey, 1) + } + return GeminiLanguageModel(modelID: geminiModelID) + + case .system: + return SystemLanguageModel.default + } + } + } +#endif diff --git a/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Schema/SchemaBuilder.swift b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Schema/SchemaBuilder.swift new file mode 100644 index 00000000000..49589a7db68 --- /dev/null +++ b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Schema/SchemaBuilder.swift @@ -0,0 +1,303 @@ +// 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. + +import ArgumentParser +import Foundation + +#if canImport(FoundationModels) && compiler(>=6.4) + public import FoundationModels + + /// Builds a `FoundationModels.GenerationSchema` from CLI property declarations and modifiers. + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + @available(tvOS, unavailable) + public struct SchemaBuilder: Sendable { + public enum PropertyKind: Sendable, Equatable { + case string + case integer + case double + case boolean + case object(schemaJSON: String?) + } + + public struct DeclaredProperty: Sendable { + public var path: [String] + public var kind: PropertyKind + public var description: String? + public var isArray: Bool = false + public var isOptional: Bool = false + + public init( + path: [String], + kind: PropertyKind, + description: String? = nil, + isArray: Bool = false, + isOptional: Bool = false + ) { + self.path = path + self.kind = kind + self.description = description + self.isArray = isArray + self.isOptional = isOptional + } + } + + /// Parses command-line arguments to construct a `GenerationSchema`. + /// + /// - Parameter arguments: Raw command line arguments following `schema object`. + /// - Returns: A generated `GenerationSchema`. + /// - Throws: `ValidationError` if required options are missing or invalid. + public static func build(from arguments: [String]) throws -> GenerationSchema { + var rootName: String? + var properties: [DeclaredProperty] = [] + var anyOfSchemas: [String] = [] + var isAnyOfMode = false + + var index = 0 + while index < arguments.count { + let arg = arguments[index] + switch arg { + case "--name": + index += 1 + guard index < arguments.count else { + throw ValidationError("Missing value for --name") + } + rootName = arguments[index] + + case "--string": + index += 1 + guard index < arguments.count else { + throw ValidationError("Missing property name for --string") + } + properties.append( + DeclaredProperty(path: parsePath(arguments[index]), kind: .string) + ) + + case "--integer", "--int": + index += 1 + guard index < arguments.count else { + throw ValidationError("Missing property name for \(arg)") + } + properties.append( + DeclaredProperty(path: parsePath(arguments[index]), kind: .integer) + ) + + case "--double": + index += 1 + guard index < arguments.count else { + throw ValidationError("Missing property name for --double") + } + properties.append( + DeclaredProperty(path: parsePath(arguments[index]), kind: .double) + ) + + case "--boolean", "--bool": + index += 1 + guard index < arguments.count else { + throw ValidationError("Missing property name for \(arg)") + } + properties.append( + DeclaredProperty(path: parsePath(arguments[index]), kind: .boolean) + ) + + case "--object": + index += 1 + guard index < arguments.count else { + throw ValidationError("Missing property name for --object") + } + properties.append( + DeclaredProperty(path: parsePath(arguments[index]), kind: .object(schemaJSON: nil)) + ) + + case "--schema": + index += 1 + guard index < arguments.count else { + throw ValidationError("Missing schema JSON for --schema") + } + let schemaString = arguments[index] + if isAnyOfMode { + anyOfSchemas.append(schemaString) + } else if !properties.isEmpty { + let lastIdx = properties.count - 1 + if case .object = properties[lastIdx].kind { + properties[lastIdx].kind = .object(schemaJSON: schemaString) + } + } + + case "--anyOf": + isAnyOfMode = true + + case "--array": + if !properties.isEmpty { + properties[properties.count - 1].isArray = true + } + + case "--description": + index += 1 + guard index < arguments.count else { + throw ValidationError("Missing text for --description") + } + if !properties.isEmpty { + properties[properties.count - 1].description = arguments[index] + } + + case "--optional": + if !properties.isEmpty { + properties[properties.count - 1].isOptional = true + } + + default: + break + } + index += 1 + } + + guard let name = rootName, !name.isEmpty else { + throw ValidationError("Missing required option: --name ") + } + + if isAnyOfMode && !anyOfSchemas.isEmpty { + let dependencies: [DynamicGenerationSchema] = [] + var choices: [DynamicGenerationSchema] = [] + for schemaJSON in anyOfSchemas { + if let data = schemaJSON.data(using: .utf8), + let genSchema = try? JSONDecoder().decode(GenerationSchema.self, from: data) + { + choices.append(DynamicGenerationSchema(referenceTo: genSchema.name)) + } + } + let root = DynamicGenerationSchema(name: name, anyOf: choices) + return try GenerationSchema(root: root, dependencies: dependencies) + } + + return try buildSchema(name: name, properties: properties) + } + + private static func parsePath(_ raw: String) -> [String] { + raw.split(separator: ".").map(String.init) + } + + private static func buildSchema( + name: String, + properties: [DeclaredProperty] + ) throws -> GenerationSchema { + var dependencies: [DynamicGenerationSchema] = [] + var rootProperties: [DynamicGenerationSchema.Property] = [] + + // Group properties by top-level name + var topLevelOrder: [String] = [] + var groups: [String: [DeclaredProperty]] = [:] + + for prop in properties { + guard let first = prop.path.first else { continue } + if groups[first] == nil { + topLevelOrder.append(first) + groups[first] = [] + } + groups[first]?.append(prop) + } + + for topLevelKey in topLevelOrder { + guard let group = groups[topLevelKey], !group.isEmpty else { continue } + + if group.count == 1 && group[0].path.count == 1 { + let prop = group[0] + let baseSchema = try makePrimitiveSchema(kind: prop.kind) + let finalSchema = prop.isArray ? DynamicGenerationSchema(arrayOf: baseSchema) : baseSchema + rootProperties.append( + DynamicGenerationSchema.Property( + name: topLevelKey, + description: prop.description, + schema: finalSchema, + isOptional: prop.isOptional + ) + ) + } else { + // Nested object + let typeName = topLevelKey.prefix(1).uppercased() + topLevelKey.dropFirst() + var childProperties: [DynamicGenerationSchema.Property] = [] + + for prop in group { + let childName = prop.path.dropFirst().joined(separator: ".") + guard !childName.isEmpty else { continue } + let baseSchema = try makePrimitiveSchema(kind: prop.kind) + let finalSchema = + prop.isArray ? DynamicGenerationSchema(arrayOf: baseSchema) : baseSchema + childProperties.append( + DynamicGenerationSchema.Property( + name: childName, + description: prop.description, + schema: finalSchema, + isOptional: prop.isOptional + ) + ) + } + + let nestedObjectSchema = DynamicGenerationSchema( + name: typeName, + properties: childProperties + ) + dependencies.append(nestedObjectSchema) + + let refSchema = DynamicGenerationSchema(referenceTo: typeName) + let finalRef = group[0].isArray ? DynamicGenerationSchema(arrayOf: refSchema) : refSchema + rootProperties.append( + DynamicGenerationSchema.Property( + name: topLevelKey, + description: group[0].description, + schema: finalRef, + isOptional: group[0].isOptional + ) + ) + } + } + + let rootSchema = DynamicGenerationSchema(name: name, properties: rootProperties) + return try GenerationSchema(root: rootSchema, dependencies: dependencies) + } + + private static func makePrimitiveSchema(kind: PropertyKind) throws -> DynamicGenerationSchema { + switch kind { + case .string: + return DynamicGenerationSchema(type: String.self) + case .integer: + return DynamicGenerationSchema(type: Int.self) + case .double: + return DynamicGenerationSchema(type: Double.self) + case .boolean: + return DynamicGenerationSchema(type: Bool.self) + case .object(let schemaJSON): + if let schemaJSON, let data = schemaJSON.data(using: .utf8), + let genSchema = try? JSONDecoder().decode(GenerationSchema.self, from: data) + { + return DynamicGenerationSchema(referenceTo: genSchema.name) + } + return DynamicGenerationSchema(name: "Object", properties: []) + } + } + + /// Formats a `GenerationSchema` as a pretty-printed JSON string matching `fm schema object`. + /// + /// - Parameter schema: The schema to format. + /// - Returns: A pretty-printed JSON string. + public static func formatJSON(_ schema: GenerationSchema) throws -> String { + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + let data = try encoder.encode(schema) + guard let string = String(data: data, encoding: .utf8) else { + throw ValidationError("Failed to decode encoded schema JSON as UTF-8 string.") + } + return string + } + } +#endif diff --git a/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Sessions/SessionManager.swift b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Sessions/SessionManager.swift new file mode 100644 index 00000000000..5e7325490d6 --- /dev/null +++ b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Sessions/SessionManager.swift @@ -0,0 +1,133 @@ +// 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. + +public import Foundation + +#if canImport(FoundationModels) && compiler(>=6.4) + public import FoundationModels + + /// Manages saving, resuming, and discovering chat sessions and transcripts in `~/.fm/sessions/`. + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + @available(tvOS, unavailable) + public struct SessionManager: Sendable { + /// The default directory where interactive chat sessions are stored. + public let sessionsDirectory: URL + + /// Initializes a new session manager with an optional custom sessions directory. + /// + /// - Parameter sessionsDirectory: Directory for storing session JSON files. Defaults to + /// `~/.fm/sessions/`. + public init(sessionsDirectory: URL? = nil) { + if let sessionsDirectory { + self.sessionsDirectory = sessionsDirectory + } else { + self.sessionsDirectory = FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent(".fm", isDirectory: true) + .appendingPathComponent("sessions", isDirectory: true) + } + } + + /// Ensures that the sessions directory exists on the filesystem. + public func ensureSessionsDirectoryExists() throws { + try FileManager.default.createDirectory( + at: sessionsDirectory, + withIntermediateDirectories: true + ) + } + + /// Resolves the URL for a session name or file path. + /// + /// - Parameter nameOrPath: Either a session identifier (e.g. `"my-session"`) or a file path. + /// - Returns: The resolved file `URL`. + public func resolveSessionURL(for nameOrPath: String) -> URL { + if nameOrPath.hasPrefix("/") || nameOrPath.hasPrefix("~") || nameOrPath.contains("/") { + let expanded = NSString(string: nameOrPath).expandingTildeInPath + return URL(fileURLWithPath: expanded) + } + let filename = nameOrPath.hasSuffix(".json") ? nameOrPath : "\(nameOrPath).json" + return sessionsDirectory.appendingPathComponent(filename) + } + + /// Saves a transcript to the sessions directory or target path. + /// + /// - Parameters: + /// - transcript: The `Transcript` to encode and save. + /// - nameOrPath: The session name or explicit file path. + public func saveSession(transcript: Transcript, nameOrPath: String) throws { + try ensureSessionsDirectoryExists() + let fileURL = resolveSessionURL(for: nameOrPath) + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + let data = try encoder.encode(transcript) + try data.write(to: fileURL, options: .atomic) + } + + /// Loads a transcript from the sessions directory or specified file path. + /// + /// - Parameter nameOrPath: The session name or file path to load. + /// - Returns: The decoded `Transcript`. + public func loadSession(nameOrPath: String) throws -> Transcript { + let fileURL = resolveSessionURL(for: nameOrPath) + let data = try Data(contentsOf: fileURL) + return try JSONDecoder().decode(Transcript.self, from: data) + } + + /// Returns the name of the most recently modified session in the sessions directory. + /// + /// - Returns: The session name without the `.json` extension, or `nil` if no sessions exist. + public func mostRecentSessionName() throws -> String? { + guard FileManager.default.fileExists(atPath: sessionsDirectory.path) else { + return nil + } + let contents = try FileManager.default.contentsOfDirectory( + at: sessionsDirectory, + includingPropertiesForKeys: [.contentModificationDateKey], + options: [.skipsHiddenFiles] + ) + let jsonFiles = contents.filter { $0.pathExtension == "json" } + let sorted = jsonFiles.compactMap { url -> (URL, Date)? in + guard let values = try? url.resourceValues(forKeys: [.contentModificationDateKey]), + let date = values.contentModificationDate + else { + return nil + } + return (url, date) + }.sorted { $0.1 > $1.1 } + + guard let mostRecent = sorted.first else { + return nil + } + return mostRecent.0.deletingPathExtension().lastPathComponent + } + + /// Lists all available saved session names. + /// + /// - Returns: An array of session names sorted alphabetically. + public func listSessionNames() throws -> [String] { + guard FileManager.default.fileExists(atPath: sessionsDirectory.path) else { + return [] + } + let contents = try FileManager.default.contentsOfDirectory( + at: sessionsDirectory, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) + return + contents + .filter { $0.pathExtension == "json" } + .map { $0.deletingPathExtension().lastPathComponent } + .sorted() + } + } +#endif diff --git a/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Tools/CurrentTimeTool.swift b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Tools/CurrentTimeTool.swift new file mode 100644 index 00000000000..15c47174a1c --- /dev/null +++ b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Tools/CurrentTimeTool.swift @@ -0,0 +1,50 @@ +// 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. + +import Foundation + +#if canImport(FoundationModels) && compiler(>=6.4) + public import FoundationModels + + /// A built-in demo tool that retrieves the current system date, time, and timezone. + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + @available(tvOS, unavailable) + public struct CurrentTimeTool: Tool, Sendable { + public let name = "current_time" + public let description = "Get the current date, time, and timezone." + + /// Arguments schema for the `CurrentTimeTool` (no arguments needed). + @Generable + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + @available(tvOS, unavailable) + public struct Arguments: Sendable { + public init() {} + } + + public init() {} + + /// Executes the tool call, returning the formatted current date and time. + /// + /// - Parameter arguments: The arguments supplied by the language model. + /// - Returns: A string representation of the current timestamp with timezone. + public func call(arguments: Arguments) async throws -> String { + let formatter = DateFormatter() + formatter.dateStyle = .full + formatter.timeStyle = .full + formatter.locale = Locale.current + formatter.timeZone = TimeZone.current + return formatter.string(from: Date()) + } + } +#endif diff --git a/GeminiLanguageModel/Samples/gfm/Tests/GFMTests/AvailableCommandTests.swift b/GeminiLanguageModel/Samples/gfm/Tests/GFMTests/AvailableCommandTests.swift new file mode 100644 index 00000000000..c86bc7739f0 --- /dev/null +++ b/GeminiLanguageModel/Samples/gfm/Tests/GFMTests/AvailableCommandTests.swift @@ -0,0 +1,75 @@ +// 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. + +import Foundation +import Testing + +@testable import GFMCore + +#if canImport(FoundationModels) && compiler(>=6.4) + import FoundationModels + + @Suite("AvailableCommand Tests") + struct AvailableCommandTests { + @Test + func modelChoiceDisplayNames() { + let geminiChoice = ModelChoice.gemini + let systemChoice = ModelChoice.system + + #expect(geminiChoice.displayName == "Gemini model") + #expect(systemChoice.displayName == "System model") + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func geminiAvailabilityWithExplicitKey() { + let resolver = ModelResolver(explicitAPIKey: "fake-key") + + let status = resolver.checkAvailability(for: .gemini) + + #expect(status == .available) + #expect(status.isAvailable == true) + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func geminiAvailabilityWithoutKey() { + let resolver = ModelResolver(explicitAPIKey: nil) + + // Only check if no environment variable is set + if resolver.resolveAPIKey() == nil { + let status = resolver.checkAvailability(for: .gemini) + + #expect(status == .unavailable(reason: "missingAPIKey")) + #expect(status.isAvailable == false) + } + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func systemAvailabilityEvaluation() { + let resolver = ModelResolver() + + let status = resolver.checkAvailability(for: .system) + + switch status { + case .available: + #expect(status.isAvailable == true) + case .unavailable(let reason): + #expect(!reason.isEmpty) + #expect(status.isAvailable == false) + } + } + } +#endif diff --git a/GeminiLanguageModel/Samples/gfm/Tests/GFMTests/CurrentTimeToolTests.swift b/GeminiLanguageModel/Samples/gfm/Tests/GFMTests/CurrentTimeToolTests.swift new file mode 100644 index 00000000000..621ccfcaeed --- /dev/null +++ b/GeminiLanguageModel/Samples/gfm/Tests/GFMTests/CurrentTimeToolTests.swift @@ -0,0 +1,45 @@ +// 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. + +import Foundation +import Testing + +@testable import GFMCore + +#if canImport(FoundationModels) && compiler(>=6.4) + import FoundationModels + + @Suite("CurrentTimeTool Tests") + struct CurrentTimeToolTests { + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func toolMetadata() { + let tool = CurrentTimeTool() + + #expect(tool.name == "current_time") + #expect(!tool.description.isEmpty) + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func toolCallExecution() async throws { + let tool = CurrentTimeTool() + + let result = try await tool.call(arguments: CurrentTimeTool.Arguments()) + + #expect(!result.isEmpty) + #expect(result.count > 5) + } + } +#endif diff --git a/GeminiLanguageModel/Samples/gfm/Tests/GFMTests/RespondAndChatCommandTests.swift b/GeminiLanguageModel/Samples/gfm/Tests/GFMTests/RespondAndChatCommandTests.swift new file mode 100644 index 00000000000..8c866cea468 --- /dev/null +++ b/GeminiLanguageModel/Samples/gfm/Tests/GFMTests/RespondAndChatCommandTests.swift @@ -0,0 +1,120 @@ +// 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. + +import ArgumentParser +import Foundation +import Testing + +@testable import GFMCore + +#if canImport(FoundationModels) && compiler(>=6.4) + import FoundationModels + + @Suite("RespondAndChatCommand Tests") + struct RespondAndChatCommandTests { + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func respondCommandDefaults() throws { + let cmd = try RespondCommand.parse(["What is Swift?"]) + + #expect(cmd.prompt == "What is Swift?") + #expect(cmd.model == .gemini) + #expect(cmd.geminiModel == "gemini-3.5-flash-lite") + #expect(cmd.stream == true) + #expect(cmd.greedy == false) + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func respondCommandOptionsParsing() throws { + let cmd = try RespondCommand.parse([ + "--model", "system", + "-i", "Be concise", + "--tool", "current-time", + "--greedy", + "Hello", + ]) + + #expect(cmd.prompt == "Hello") + #expect(cmd.model == .system) + #expect(cmd.instructions == "Be concise") + #expect(cmd.tool == ["current-time"]) + #expect(cmd.greedy == true) + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func chatCommandDefaults() throws { + let cmd = try ChatCommand.parse([]) + + #expect(cmd.model == .gemini) + #expect(cmd.geminiModel == "gemini-3.5-flash-lite") + #expect(cmd.continue == false) + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func chatCommandOptionsParsing() throws { + let cmd = try ChatCommand.parse([ + "--model", "system", + "-r", "saved-session", + "--tool", "current-time", + ]) + + #expect(cmd.model == .system) + #expect(cmd.resume == "saved-session") + #expect(cmd.tool == ["current-time"]) + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func resolvePromptWithArgumentOnly() throws { + let cmd = try RespondCommand.parse(["What is Swift?"]) + + let prompt = try cmd.resolvePrompt(stdinReader: { nil }) + + #expect(prompt == "What is Swift?") + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func resolvePromptWithStdinOnly() throws { + let cmd = try RespondCommand.parse([]) + + let prompt = try cmd.resolvePrompt(stdinReader: { "Piped document text." }) + + #expect(prompt == "Piped document text.") + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func resolvePromptWithStdinAndArgumentCombined() throws { + let cmd = try RespondCommand.parse(["What are the main takeaways?"]) + + let prompt = try cmd.resolvePrompt(stdinReader: { "Article contents here." }) + + #expect(prompt == "Article contents here.\n\nWhat are the main takeaways?") + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func resolvePromptWithNeitherThrows() throws { + let cmd = try RespondCommand.parse([]) + + #expect(throws: ValidationError.self) { + try cmd.resolvePrompt(stdinReader: { nil }) + } + } + } +#endif diff --git a/GeminiLanguageModel/Samples/gfm/Tests/GFMTests/SchemaCommandTests.swift b/GeminiLanguageModel/Samples/gfm/Tests/GFMTests/SchemaCommandTests.swift new file mode 100644 index 00000000000..52f387260a5 --- /dev/null +++ b/GeminiLanguageModel/Samples/gfm/Tests/GFMTests/SchemaCommandTests.swift @@ -0,0 +1,80 @@ +// 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. + +import Foundation +import Testing + +@testable import GFMCore + +#if canImport(FoundationModels) && compiler(>=6.4) + import FoundationModels + + @Suite("SchemaCommand Tests") + struct SchemaCommandTests { + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func buildSimpleObjectSchema() throws { + let args = ["--name", "Person", "--string", "name", "--int", "age"] + + let schema = try SchemaBuilder.build(from: args) + let json = try SchemaBuilder.formatJSON(schema) + + #expect(schema.name == "Person") + #expect(json.contains("\"title\" : \"Person\"")) + #expect(json.contains("\"name\" : {\n \"type\" : \"string\"\n }")) + #expect(json.contains("\"age\" : {\n \"type\" : \"integer\"\n }")) + #expect(json.contains("\"additionalProperties\" : false")) + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func buildSchemaWithModifiers() throws { + let args = [ + "--name", "Dog", + "--string", "breed", "--description", "Breed of dog", + "--boolean", "friendly", "--optional", + "--string", "tags", "--array", + ] + + let schema = try SchemaBuilder.build(from: args) + let json = try SchemaBuilder.formatJSON(schema) + + #expect(schema.name == "Dog") + #expect(json.contains("\"description\" : \"Breed of dog\"")) + #expect(json.contains("\"type\" : \"array\"")) + #expect(json.contains("\"friendly\" : {\n \"type\" : \"boolean\"\n }")) + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func buildSchemaWithDotNotationNesting() throws { + let args = [ + "--name", "Restaurant", + "--string", "name", + "--string", "address.street", + "--string", "address.zip", + ] + + let schema = try SchemaBuilder.build(from: args) + let json = try SchemaBuilder.formatJSON(schema) + + #expect(schema.name == "Restaurant") + #expect(json.contains("\"title\" : \"Restaurant\"")) + #expect(json.contains("\"$ref\" : \"#\\/$defs\\/Address\"")) + #expect(json.contains("\"Address\" : {")) + #expect(json.contains("\"street\" : {\n \"type\" : \"string\"\n }")) + #expect(json.contains("\"zip\" : {\n \"type\" : \"string\"\n }")) + } + } +#endif diff --git a/GeminiLanguageModel/Samples/gfm/Tests/GFMTests/SessionManagerTests.swift b/GeminiLanguageModel/Samples/gfm/Tests/GFMTests/SessionManagerTests.swift new file mode 100644 index 00000000000..4697fdc98b9 --- /dev/null +++ b/GeminiLanguageModel/Samples/gfm/Tests/GFMTests/SessionManagerTests.swift @@ -0,0 +1,64 @@ +// 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. + +import Foundation +import Testing + +@testable import GFMCore + +#if canImport(FoundationModels) && compiler(>=6.4) + import FoundationModels + + @Suite("SessionManager Tests") + struct SessionManagerTests { + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func sessionURLResolution() { + let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent( + UUID().uuidString, + isDirectory: true + ) + let manager = SessionManager(sessionsDirectory: tempDir) + + let namedURL = manager.resolveSessionURL(for: "test-session") + let jsonURL = manager.resolveSessionURL(for: "test-session.json") + let absoluteURL = manager.resolveSessionURL(for: "/tmp/custom.json") + + #expect(namedURL.path == tempDir.appendingPathComponent("test-session.json").path) + #expect(jsonURL.path == tempDir.appendingPathComponent("test-session.json").path) + #expect(absoluteURL.path == "/tmp/custom.json") + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func saveAndLoadSessionTranscript() throws { + let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent( + UUID().uuidString, + isDirectory: true + ) + let manager = SessionManager(sessionsDirectory: tempDir) + defer { try? FileManager.default.removeItem(at: tempDir) } + let transcript = Transcript(entries: []) + + try manager.saveSession(transcript: transcript, nameOrPath: "my-test-session") + let loaded = try manager.loadSession(nameOrPath: "my-test-session") + let list = try manager.listSessionNames() + + #expect(loaded == transcript) + #expect(list.contains("my-test-session")) + let mostRecent = try manager.mostRecentSessionName() + #expect(mostRecent == "my-test-session") + } + } +#endif diff --git a/GeminiLanguageModel/Sources/GeminiAPIClient/EndpointConfiguration.swift b/GeminiLanguageModel/Sources/GeminiAPIClient/EndpointConfiguration.swift index 1524b075340..2acb65d8388 100644 --- a/GeminiLanguageModel/Sources/GeminiAPIClient/EndpointConfiguration.swift +++ b/GeminiLanguageModel/Sources/GeminiAPIClient/EndpointConfiguration.swift @@ -47,3 +47,13 @@ package struct EndpointConfiguration: Sendable, Hashable, Equatable { self.apiVersion = apiVersion } } + +// MARK: - Standard Endpoints + +extension EndpointConfiguration { + /// The Gemini Developer API endpoint configuration (`generativelanguage.googleapis.com/v1beta`). + package static let geminiDeveloperAPI = EndpointConfiguration( + host: "generativelanguage.googleapis.com", + apiVersion: "v1beta" + ) +} diff --git a/GeminiLanguageModel/Sources/GeminiAPIClient/GeminiAPIClient.swift b/GeminiLanguageModel/Sources/GeminiAPIClient/GeminiAPIClient.swift index ff742ae9e19..c8a600645f7 100644 --- a/GeminiLanguageModel/Sources/GeminiAPIClient/GeminiAPIClient.swift +++ b/GeminiLanguageModel/Sources/GeminiAPIClient/GeminiAPIClient.swift @@ -13,7 +13,7 @@ // limitations under the License. package import GeminiAPIDataModels -package import GeminiSharedDataModels +import GeminiSharedDataModels package import InteractionsDataModels #if canImport(Darwin) diff --git a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel+Error.swift b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel+Error.swift index b312275f168..adecc1e1542 100644 --- a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel+Error.swift +++ b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel+Error.swift @@ -33,6 +33,9 @@ /// An error returned by the Gemini API indicating a client or request failure. case apiError(APIError) + /// An error indicating that a required API key was not found. + case missingAPIKey(MissingAPIKey) + // MARK: - Payload Structures /// Information about the Gemini service being temporarily unavailable. @@ -107,6 +110,19 @@ } } + /// Information about a missing API key error. + public struct MissingAPIKey: Sendable, CustomDebugStringConvertible { + /// A debug description of the missing API key error. + public let debugDescription: String + + /// Creates a missing API key error instance. + /// + /// - Parameter debugDescription: A debug description of the failure. + public init(debugDescription: String) { + self.debugDescription = debugDescription + } + } + // MARK: - LocalizedError & CustomDebugStringConvertible public var errorDescription: String? { @@ -119,6 +135,8 @@ modelNotFound.debugDescription case .apiError(let apiError): apiError.debugDescription + case .missingAPIKey(let missingAPIKey): + missingAPIKey.debugDescription } } diff --git a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel.swift b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel.swift index 4037beee7c1..bfdbba87ee6 100644 --- a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel.swift +++ b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel.swift @@ -13,7 +13,11 @@ // limitations under the License. #if canImport(FoundationModels) && compiler(>=6.4) - package import Foundation + #if GeminiDeveloperAPIEnvironmentAuth + public import Foundation + #else + package import Foundation + #endif public import FoundationModels package import GeminiAPIClient @@ -86,6 +90,78 @@ apiVariant: apiVariant ) } + + #if GeminiDeveloperAPIEnvironmentAuth + /// Initializes a new Gemini language model using an API key from the environment. + /// + /// The model inspects the `GOOGLE_API_KEY` and `GEMINI_API_KEY` environment variables (preferring + /// `GOOGLE_API_KEY` if both are set) to authenticate requests with the Gemini Developer API. + /// + /// - Parameters: + /// - modelID: The model identifier to use (e.g. `"gemini-3.5-flash-lite"`). Defaults to + /// `"gemini-3.5-flash-lite"`. + /// - configuration: The `URLSessionConfiguration` to use. Defaults to `.ephemeral`. + /// - apiVariant: The API variant to use. Defaults to `.generateContent`. + public init( + modelID: String = "gemini-3.5-flash-lite", + configuration: URLSessionConfiguration = .ephemeral, + apiVariant: APIVariant = .generateContent + ) { + self.init( + modelID: modelID, + environment: ProcessInfo.processInfo.environment, + configuration: configuration, + apiVariant: apiVariant + ) + } + + /// Initializes a new Gemini language model with an explicit environment dictionary. + /// + /// - Parameters: + /// - modelID: The model identifier to use. Defaults to `"gemini-3.5-flash-lite"`. + /// - environment: The environment dictionary containing `GOOGLE_API_KEY` or `GEMINI_API_KEY`. + /// - configuration: The `URLSessionConfiguration` to use. Defaults to `.ephemeral`. + /// - apiVariant: The API variant to use. Defaults to `.generateContent`. + package init( + modelID: String = "gemini-3.5-flash-lite", + environment: [String: String], + configuration: URLSessionConfiguration = .ephemeral, + apiVariant: APIVariant = .generateContent + ) { + let resource = ModelResource( + modelID: modelID, + urlResourceName: "models/\(modelID)", + payloadResourceName: "models/\(modelID)" + ) + self.init( + modelResource: resource, + endpointConfiguration: .geminiDeveloperAPI, + headerProvider: { + guard let apiKey = Self.resolveAPIKey(from: environment) else { + throw GeminiLanguageModel.Error.missingAPIKey( + GeminiLanguageModel.Error.MissingAPIKey( + debugDescription: + "A Gemini API key is required. Set the GOOGLE_API_KEY or GEMINI_API_KEY environment variable." + ) + ) + } + return ["x-goog-api-key": apiKey] + }, + configuration: configuration, + apiVariant: apiVariant + ) + } + + private static func resolveAPIKey(from environment: [String: String]) -> String? { + if let googleKey = environment["GOOGLE_API_KEY"], !googleKey.isEmpty { + return googleKey + } + if let geminiKey = environment["GEMINI_API_KEY"], !geminiKey.isEmpty { + return geminiKey + } + return nil + } + #endif // GeminiDeveloperAPIEnvironmentAuth } // MARK: - LanguageModel Conformance diff --git a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiLanguageModelTests.swift b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiLanguageModelTests.swift index 4d645492010..f0698283080 100644 --- a/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiLanguageModelTests.swift +++ b/GeminiLanguageModel/Tests/GeminiLanguageModelTests/GeminiLanguageModelTests.swift @@ -505,6 +505,72 @@ ) } + #if GeminiDeveloperAPIEnvironmentAuth + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func developerAPIPrefersGoogleAPIKeyOverGeminiAPIKey() async throws { + let environment = [ + "GOOGLE_API_KEY": "preferred-google-key", + "GEMINI_API_KEY": "fallback-gemini-key", + ] + + let model = GeminiLanguageModel(environment: environment) + let headerProvider = try #require(model.executorConfiguration.headerProvider) + let headers = try await headerProvider() + + #expect(headers["x-goog-api-key"] == "preferred-google-key") + #expect(model.executorConfiguration.modelResource.modelID == "gemini-3.5-flash-lite") + #expect(model.executorConfiguration.endpointConfiguration == .geminiDeveloperAPI) + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func developerAPIFallsBackToGeminiAPIKey() async throws { + let environment = ["GEMINI_API_KEY": "gemini-key"] + + let model = GeminiLanguageModel(modelID: "custom-model", environment: environment) + let headerProvider = try #require(model.executorConfiguration.headerProvider) + let headers = try await headerProvider() + + #expect(headers["x-goog-api-key"] == "gemini-key") + #expect(model.executorConfiguration.modelResource.modelID == "custom-model") + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func developerAPIWithEmptyEnvironmentThrowsMissingAPIKey() async throws { + let model = GeminiLanguageModel(environment: [:]) + let headerProvider = try #require(model.executorConfiguration.headerProvider) + + await #expect(throws: GeminiLanguageModel.Error.self) { + try await headerProvider() + } + } + + @Test(.requireAPIKey) + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func developerAPIDefaultInitWithProcessEnvironment() async throws { + let expectedKey = try #require(geminiAPIKey) + + let model = GeminiLanguageModel() + let headerProvider = try #require(model.executorConfiguration.headerProvider) + let headers = try await headerProvider() + + #expect(headers["x-goog-api-key"] == expectedKey) + } + + @Test(.requireNoAPIKey) + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func developerAPIDefaultInitWithoutKeyThrows() async throws { + let model = GeminiLanguageModel() + let headerProvider = try #require(model.executorConfiguration.headerProvider) + + await #expect(throws: GeminiLanguageModel.Error.self) { + try await headerProvider() + } + } + #endif // GeminiDeveloperAPIEnvironmentAuth + private static func makeExpectedStreamURL( host: String = EndpointConfiguration.geminiDeveloperAPIHost, apiVersion: String = EndpointConfiguration.geminiDeveloperAPIVersion, diff --git a/GeminiLanguageModel/Tests/GeminiTestUtilities/TestConstants.swift b/GeminiLanguageModel/Tests/GeminiTestUtilities/TestConstants.swift index 0679f298074..4a3b95b2bb8 100644 --- a/GeminiLanguageModel/Tests/GeminiTestUtilities/TestConstants.swift +++ b/GeminiLanguageModel/Tests/GeminiTestUtilities/TestConstants.swift @@ -54,12 +54,6 @@ extension EndpointConfiguration { /// Test Developer API version string. package static let geminiDeveloperAPIVersion = "v1beta" - /// Test Developer API endpoint configuration (`generativelanguage.googleapis.com/v1beta`). - package static let geminiDeveloperAPI = EndpointConfiguration( - host: geminiDeveloperAPIHost, - apiVersion: geminiDeveloperAPIVersion - ) - /// Test Firebase AI Logic host name. package static let firebaseAILogicHost = "firebasevertexai.googleapis.com" From dd23a20343d4e0c1c231a287b9b0b196a1fc303d Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Thu, 10 Sep 2026 19:19:13 -0400 Subject: [PATCH 4/4] feat(ai): support Interactions API and iOS in gfm Add support for selecting the Gemini Interactions API variant from the CLI in the `gfm` utility, and enable building and testing for iOS 27 Simulator environments. * Add `--api-variant` and `--api` options to `respond`, `chat`, and `available` commands in `gfm`. * Define `APIVariantChoice` in `GFMCore` conforming to `ExpressibleByArgument` to cleanly parse API variants without retroactive conformance warnings. * Forward configured API variants through `ModelResolver` to `GeminiLanguageModel`. * Expose public `apiVariant` property on `GeminiLanguageModel`. * Add `.iOS(.v18)` to `gfm/Package.swift` and update `main.swift` and `SessionManager` to build cleanly for iOS Simulator. * Add unit tests for argument parsing, command defaults, and model variant configuration. * Update `gfm` documentation with Xcode iOS Simulator execution steps. --- GeminiLanguageModel/Samples/gfm/Package.swift | 2 +- GeminiLanguageModel/Samples/gfm/README.md | 52 +++++++++++++- .../Samples/gfm/Sources/GFM/main.swift | 8 +-- .../GFMCore/Commands/AvailableCommand.swift | 7 ++ .../GFMCore/Commands/ChatCommand.swift | 7 ++ .../GFMCore/Commands/RespondCommand.swift | 7 ++ .../GFMCore/Models/ModelSelection.swift | 41 ++++++++++- .../GFMCore/Sessions/SessionManager.swift | 8 ++- .../GFMTests/RespondAndChatCommandTests.swift | 72 +++++++++++++++++++ .../GeminiLanguageModel.swift | 5 ++ 10 files changed, 200 insertions(+), 9 deletions(-) diff --git a/GeminiLanguageModel/Samples/gfm/Package.swift b/GeminiLanguageModel/Samples/gfm/Package.swift index 4d3d6dd95da..7a04d6adaed 100644 --- a/GeminiLanguageModel/Samples/gfm/Package.swift +++ b/GeminiLanguageModel/Samples/gfm/Package.swift @@ -24,7 +24,7 @@ let defaultSwiftSettings: [SwiftSetting] = [ let package = Package( name: "gfm", - platforms: [.macOS(.v15)], + platforms: [.macOS(.v15), .iOS(.v18)], products: [ .executable( name: "gfm", diff --git a/GeminiLanguageModel/Samples/gfm/README.md b/GeminiLanguageModel/Samples/gfm/README.md index b13dde090c8..b0d1db92db8 100644 --- a/GeminiLanguageModel/Samples/gfm/README.md +++ b/GeminiLanguageModel/Samples/gfm/README.md @@ -8,7 +8,8 @@ Google Gemini (`GeminiLanguageModel`) and Apple System models ## Requirements -* macOS 26.0 or later (macOS 27.0+ recommended for `SystemLanguageModel`) +* macOS 26.0 or later (macOS 27.0+ recommended for `SystemLanguageModel`), or + iOS 27.0+ (Simulator or device) * Swift 6.0 or later / Xcode 26.2+ * A Gemini API key (obtainable from Google AI Studio) for `gemini` models @@ -42,6 +43,8 @@ gfm respond --api-key "your-gemini-api-key" "Explain quantum computing briefly." * `-m, --model `: Select the backend model (`gemini` by default). * `--gemini-model `: Specify a Gemini model variant (defaults to `gemini-3.5-flash-lite`, e.g. `gemini-2.5-pro` or `gemini-2.5-flash`). +* `--api-variant, --api `: Specify the Gemini API + variant (`generate-content` by default, or `interactions`). ## Building and Running @@ -100,6 +103,47 @@ alias gfm="$(pwd)/GeminiLanguageModel/Samples/gfm/.build/release/gfm" ln -sf "$(pwd)/GeminiLanguageModel/Samples/gfm/.build/release/gfm" /usr/local/bin/gfm ``` +### 3. Run on iOS Simulator via Xcode + +If your Mac is running macOS 26 and does not yet have macOS 27 for on-device +`SystemLanguageModel` support, you can build and run `gfm` targeting an **iOS 27 +Simulator** inside Xcode: + +1. **Open the package in Xcode**: + ```bash + open GeminiLanguageModel/Samples/gfm/Package.swift + ``` +2. **Select the Scheme and Destination**: + * Select the `gfm` executable scheme in the Xcode toolbar. + * Choose an **iOS 27 Simulator** destination (e.g. `iPhone 17 Pro`). +3. **Configure Arguments and Environment Variables**: + * Go to **Product > Scheme > Edit Scheme...** (`⌘<`). + * Select **Run** in the left sidebar, then choose the **Arguments** tab. + * **Arguments Passed On Launch**: Add the command arguments you want to run, + for example: + * `available` + * `respond "Write a haiku about Swift."` + * `respond --api interactions "Explain how quantum computers work."` + * `chat` + * **Environment Variables**: Click `+` and add: + * Name: `GOOGLE_API_KEY` (or `GEMINI_API_KEY`) + * Value: `` + * *(Optional)* Under the **Options** tab, set **Console** to **Use standard + console** if you are running an interactive `chat` session. +4. **Run**: + * Press `⌘R` (or click the Run button). + * The output streams directly in Xcode's debug console pane (`⇧⌘Y`). + +You can also build or test directly from the command line for iOS Simulator: + +```bash +# Build for iOS Simulator +xcodebuild -scheme gfm -destination 'generic/platform=iOS Simulator' + +# Run unit tests on an iOS 27 Simulator +xcodebuild test -scheme gfm -destination 'platform=iOS Simulator,name=iPhone 17 Pro' +``` + ## Commands and Usage ### 1. `available` @@ -146,6 +190,9 @@ gfm respond --resume "my-session" "What is my favorite color?" # Call built-in demo tools gfm respond --tool current-time "What time is it right now?" + +# Use the Interactions API instead of Generate Content +gfm respond --api interactions "Explain how quantum computers work." ``` #### Structured Output with `--schema` @@ -181,6 +228,9 @@ gfm chat -r "my-session" # Continue the most recent session gfm chat -c + +# Start a chat session using the Interactions API +gfm chat --api interactions ``` #### In-Chat Slash Commands diff --git a/GeminiLanguageModel/Samples/gfm/Sources/GFM/main.swift b/GeminiLanguageModel/Samples/gfm/Sources/GFM/main.swift index 200ea9a0016..c5ddb011710 100644 --- a/GeminiLanguageModel/Samples/gfm/Sources/GFM/main.swift +++ b/GeminiLanguageModel/Samples/gfm/Sources/GFM/main.swift @@ -17,7 +17,7 @@ import Darwin import Foundation import GFMCore -if #available(macOS 27.0, *) { +if #available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) { do { let command = try await GFMCommand.asyncParseAsRoot() try await execute(command) @@ -25,11 +25,11 @@ if #available(macOS 27.0, *) { GFMCommand.exit(withError: error) } } else { - fputs("Error: gfm requires macOS 27.0 or newer.\n", stderr) + fputs("Error: gfm requires macOS 27.0, iOS 27.0, or newer.\n", stderr) exit(1) } -@available(macOS 27.0, *) +@available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) private func execute(_ command: any ParsableCommand) async throws { if let asyncCommand = command as? any AsyncParsableCommand { try await executeAsync(asyncCommand) @@ -39,7 +39,7 @@ private func execute(_ command: any ParsableCommand) async throws { } } -@available(macOS 27.0, *) +@available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) private func executeAsync(_ command: C) async throws { var asyncCommand = command try await asyncCommand.run() diff --git a/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/AvailableCommand.swift b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/AvailableCommand.swift index 9c11c14ee76..7bbf355d567 100644 --- a/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/AvailableCommand.swift +++ b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/AvailableCommand.swift @@ -39,6 +39,12 @@ import Foundation ) public var geminiModel: String = ModelResolver.defaultGeminiModelID + @Option( + name: [.customLong("api-variant"), .customLong("api")], + help: "Gemini API variant to use (generate-content, interactions; default: generate-content)." + ) + public var apiVariant: APIVariantChoice = .generateContent + @Option( name: .long, help: "Explicit Gemini API key." @@ -51,6 +57,7 @@ import Foundation let resolver = ModelResolver( choice: model ?? .gemini, geminiModelID: geminiModel, + apiVariant: apiVariant, explicitAPIKey: apiKey ) diff --git a/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/ChatCommand.swift b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/ChatCommand.swift index f6db4496848..13e7ccdda72 100644 --- a/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/ChatCommand.swift +++ b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/ChatCommand.swift @@ -39,6 +39,12 @@ import Foundation ) public var geminiModel: String = ModelResolver.defaultGeminiModelID + @Option( + name: [.customLong("api-variant"), .customLong("api")], + help: "Gemini API variant to use (generate-content, interactions; default: generate-content)." + ) + public var apiVariant: APIVariantChoice = .generateContent + @Option( name: .long, help: "Explicit Gemini API key." @@ -75,6 +81,7 @@ import Foundation let resolver = ModelResolver( choice: model, geminiModelID: geminiModel, + apiVariant: apiVariant, explicitAPIKey: apiKey ) let languageModel = try resolver.makeModel() diff --git a/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/RespondCommand.swift b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/RespondCommand.swift index dc62da4988b..7f43c306882 100644 --- a/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/RespondCommand.swift +++ b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Commands/RespondCommand.swift @@ -47,6 +47,12 @@ import func Darwin.C.isatty ) public var geminiModel: String = ModelResolver.defaultGeminiModelID + @Option( + name: [.customLong("api-variant"), .customLong("api")], + help: "Gemini API variant to use (generate-content, interactions; default: generate-content)." + ) + public var apiVariant: APIVariantChoice = .generateContent + @Option( name: .long, help: "Explicit Gemini API key." @@ -115,6 +121,7 @@ import func Darwin.C.isatty let resolver = ModelResolver( choice: model, geminiModelID: geminiModel, + apiVariant: apiVariant, explicitAPIKey: apiKey ) let languageModel = try resolver.makeModel() diff --git a/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Models/ModelSelection.swift b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Models/ModelSelection.swift index 4a6bfd252de..36f6c7a8f44 100644 --- a/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Models/ModelSelection.swift +++ b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Models/ModelSelection.swift @@ -48,7 +48,38 @@ import Foundation } } + /// Supported Gemini API variants for the `gfm` command-line utility. + public enum APIVariantChoice: String, ExpressibleByArgument, CaseIterable, Sendable { + case generateContent = "generate-content" + case interactions + + /// The corresponding SDK `GeminiLanguageModel.APIVariant`. + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + @available(tvOS, unavailable) + public var apiVariant: GeminiLanguageModel.APIVariant { + switch self { + case .generateContent: + return .generateContent + case .interactions: + return .interactions + } + } + + public init?(argument: String) { + switch argument.lowercased() { + case "generate-content", "generatecontent", "generate_content": + self = .generateContent + case "interactions", "interaction": + self = .interactions + default: + return nil + } + } + } + /// Helper for configuring and resolving language models from CLI options and environment. + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + @available(tvOS, unavailable) public struct ModelResolver: Sendable { /// Default Gemini model ID. public static let defaultGeminiModelID = "gemini-3.5-flash-lite" @@ -59,6 +90,9 @@ import Foundation /// Custom Gemini model ID override. public var geminiModelID: String + /// The API variant to use when targeting Gemini. Defaults to `.generateContent`. + public var apiVariant: APIVariantChoice + /// Optional explicit API key passed via `--api-key`. public var explicitAPIKey: String? @@ -68,14 +102,17 @@ import Foundation /// - choice: The model family to use. Defaults to `.gemini`. /// - geminiModelID: The model identifier to use when targetting Gemini. Defaults to /// `"gemini-3.5-flash-lite"`. + /// - apiVariant: The API variant to use when targeting Gemini. Defaults to `.generateContent`. /// - explicitAPIKey: An optional explicit API key. public init( choice: ModelChoice = .gemini, geminiModelID: String = defaultGeminiModelID, + apiVariant: APIVariantChoice = .generateContent, explicitAPIKey: String? = nil ) { self.choice = choice self.geminiModelID = geminiModelID + self.apiVariant = apiVariant self.explicitAPIKey = explicitAPIKey } @@ -140,7 +177,7 @@ import Foundation public func makeModel() throws -> any LanguageModel { switch choice { case .gemini: - guard let apiKey = resolveAPIKey() else { + guard resolveAPIKey() != nil else { throw ValidationError( "Gemini API key is required. Provide --api-key or set GEMINI_API_KEY (or GOOGLE_API_KEY)." ) @@ -148,7 +185,7 @@ import Foundation if let explicitAPIKey, !explicitAPIKey.isEmpty { setenv("GEMINI_API_KEY", explicitAPIKey, 1) } - return GeminiLanguageModel(modelID: geminiModelID) + return GeminiLanguageModel(modelID: geminiModelID, apiVariant: apiVariant.apiVariant) case .system: return SystemLanguageModel.default diff --git a/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Sessions/SessionManager.swift b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Sessions/SessionManager.swift index 5e7325490d6..cdccf2d7345 100644 --- a/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Sessions/SessionManager.swift +++ b/GeminiLanguageModel/Samples/gfm/Sources/GFMCore/Sessions/SessionManager.swift @@ -32,7 +32,13 @@ public import Foundation if let sessionsDirectory { self.sessionsDirectory = sessionsDirectory } else { - self.sessionsDirectory = FileManager.default.homeDirectoryForCurrentUser + #if os(macOS) + let homeDir = FileManager.default.homeDirectoryForCurrentUser + #else + let homeDir = URL(fileURLWithPath: NSHomeDirectory()) + #endif + self.sessionsDirectory = + homeDir .appendingPathComponent(".fm", isDirectory: true) .appendingPathComponent("sessions", isDirectory: true) } diff --git a/GeminiLanguageModel/Samples/gfm/Tests/GFMTests/RespondAndChatCommandTests.swift b/GeminiLanguageModel/Samples/gfm/Tests/GFMTests/RespondAndChatCommandTests.swift index 8c866cea468..73664071a91 100644 --- a/GeminiLanguageModel/Samples/gfm/Tests/GFMTests/RespondAndChatCommandTests.swift +++ b/GeminiLanguageModel/Samples/gfm/Tests/GFMTests/RespondAndChatCommandTests.swift @@ -20,6 +20,7 @@ import Testing #if canImport(FoundationModels) && compiler(>=6.4) import FoundationModels + import GeminiLanguageModel @Suite("RespondAndChatCommand Tests") struct RespondAndChatCommandTests { @@ -31,6 +32,7 @@ import Testing #expect(cmd.prompt == "What is Swift?") #expect(cmd.model == .gemini) #expect(cmd.geminiModel == "gemini-3.5-flash-lite") + #expect(cmd.apiVariant == .generateContent) #expect(cmd.stream == true) #expect(cmd.greedy == false) } @@ -53,6 +55,18 @@ import Testing #expect(cmd.greedy == true) } + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func respondCommandAPIVariantParsing() throws { + let longFlagCmd = try RespondCommand.parse(["--api-variant", "interactions", "Hello"]) + let shortAliasCmd = try RespondCommand.parse(["--api", "interactions", "Hello"]) + let generateCmd = try RespondCommand.parse(["--api-variant", "generate-content", "Hello"]) + + #expect(longFlagCmd.apiVariant == .interactions) + #expect(shortAliasCmd.apiVariant == .interactions) + #expect(generateCmd.apiVariant == .generateContent) + } + @Test @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) func chatCommandDefaults() throws { @@ -60,6 +74,7 @@ import Testing #expect(cmd.model == .gemini) #expect(cmd.geminiModel == "gemini-3.5-flash-lite") + #expect(cmd.apiVariant == .generateContent) #expect(cmd.continue == false) } @@ -77,6 +92,63 @@ import Testing #expect(cmd.tool == ["current-time"]) } + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func chatCommandAPIVariantParsing() throws { + let longFlagCmd = try ChatCommand.parse(["--api-variant", "interactions"]) + let shortAliasCmd = try ChatCommand.parse(["--api", "interactions"]) + + #expect(longFlagCmd.apiVariant == .interactions) + #expect(shortAliasCmd.apiVariant == .interactions) + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func apiVariantArgumentParsing() { + let generateContentCases = [ + APIVariantChoice(argument: "generate-content"), + APIVariantChoice(argument: "generatecontent"), + APIVariantChoice(argument: "generate_content"), + ] + let interactionsCases = [ + APIVariantChoice(argument: "interactions"), + APIVariantChoice(argument: "interaction"), + ] + let invalid = APIVariantChoice(argument: "unknown") + + for parsed in generateContentCases { + #expect(parsed == .generateContent) + #expect(parsed?.apiVariant == .generateContent) + } + for parsed in interactionsCases { + #expect(parsed == .interactions) + #expect(parsed?.apiVariant == .interactions) + } + #expect(invalid == nil) + } + + @Test + @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) + func modelResolverConfiguresAPIVariant() throws { + let interactionsResolver = ModelResolver( + apiVariant: .interactions, + explicitAPIKey: "fake-key" + ) + let interactionsModel = try interactionsResolver.makeModel() + let glmInteractions = try #require(interactionsModel as? GeminiLanguageModel) + + #expect(glmInteractions.apiVariant == .interactions) + + let generateContentResolver = ModelResolver( + apiVariant: .generateContent, + explicitAPIKey: "fake-key" + ) + let generateContentModel = try generateContentResolver.makeModel() + let glmGenerateContent = try #require(generateContentModel as? GeminiLanguageModel) + + #expect(glmGenerateContent.apiVariant == .generateContent) + } + @Test @available(macOS 27.0, iOS 27.0, watchOS 27.0, visionOS 27.0, *) func resolvePromptWithArgumentOnly() throws { diff --git a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel.swift b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel.swift index bfdbba87ee6..b16476e9765 100644 --- a/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel.swift +++ b/GeminiLanguageModel/Sources/GeminiLanguageModel/GeminiLanguageModel.swift @@ -67,6 +67,11 @@ /// Gemini API calls. public let executorConfiguration: Executor.Configuration + /// The API variant used to communicate with Gemini. + public var apiVariant: APIVariant { + executorConfiguration.apiVariant + } + /// Initializes a new Gemini language model. /// /// - Parameters: