diff --git a/embabel-agent-api/src/main/kotlin/com/embabel/agent/api/models/AtlasCloudModels.kt b/embabel-agent-api/src/main/kotlin/com/embabel/agent/api/models/AtlasCloudModels.kt new file mode 100644 index 000000000..5cedee1f6 --- /dev/null +++ b/embabel-agent-api/src/main/kotlin/com/embabel/agent/api/models/AtlasCloudModels.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2024-2026 Embabel Pty Ltd. + * + * 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 com.embabel.agent.api.models + +/** + * Provides constants for Atlas Cloud model identifiers. + * Atlas Cloud exposes models from multiple vendors through an OpenAI-compatible API. + * + * @see Atlas Cloud Models + */ +class AtlasCloudModels { + + companion object { + + const val QWEN3_5_FLASH = "qwen/qwen3.5-flash" + const val QWEN3_8_MAX = "qwen/qwen3.8-max" + + const val PROVIDER = "Atlas Cloud" + } +} diff --git a/embabel-agent-common/embabel-agent-byok/src/main/kotlin/com/embabel/common/byok/ProviderDetection.kt b/embabel-agent-common/embabel-agent-byok/src/main/kotlin/com/embabel/common/byok/ProviderDetection.kt index 72dcb9134..41853afc1 100644 --- a/embabel-agent-common/embabel-agent-byok/src/main/kotlin/com/embabel/common/byok/ProviderDetection.kt +++ b/embabel-agent-common/embabel-agent-byok/src/main/kotlin/com/embabel/common/byok/ProviderDetection.kt @@ -31,6 +31,7 @@ import java.util.concurrent.Executors * OpenAiCompatibleModelFactory.deepSeek(userKey), * OpenAiCompatibleModelFactory.mistral(userKey), * OpenAiCompatibleModelFactory.gemini(userKey), + * OpenAiCompatibleModelFactory.atlasCloud(userKey), * ) * val detectedProvider = service.provider * ``` diff --git a/embabel-agent-docs/src/main/asciidoc/reference/customizing/page.adoc b/embabel-agent-docs/src/main/asciidoc/reference/customizing/page.adoc index 7352f6256..65c86b668 100644 --- a/embabel-agent-docs/src/main/asciidoc/reference/customizing/page.adoc +++ b/embabel-agent-docs/src/main/asciidoc/reference/customizing/page.adoc @@ -224,6 +224,9 @@ val service: LlmService<*> = OpenAiCompatibleModelFactory.mistral(userKey).build // Gemini (OpenAI-compatible endpoint) val service: LlmService<*> = OpenAiCompatibleModelFactory.gemini(userKey).buildValidated() + +// Atlas Cloud (OpenAI-compatible endpoint) +val service: LlmService<*> = OpenAiCompatibleModelFactory.atlasCloud(userKey).buildValidated() ---- `buildValidated()` makes a single probe call with no retries. @@ -258,6 +261,7 @@ val service: LlmService<*> = detectProvider( OpenAiCompatibleModelFactory.deepSeek(userKey), OpenAiCompatibleModelFactory.mistral(userKey), OpenAiCompatibleModelFactory.gemini(userKey), + OpenAiCompatibleModelFactory.atlasCloud(userKey), ) val detectedProvider: String = service.provider ---- @@ -543,4 +547,3 @@ Remove the `embabel.agent.logging.personality` key to disable personality-based As all logging results from listening to events via an `AgenticEventListener`, you can also easily create your own customized logging. - diff --git a/embabel-agent-openai/src/main/kotlin/com/embabel/agent/openai/OpenAiCompatibleModelFactory.kt b/embabel-agent-openai/src/main/kotlin/com/embabel/agent/openai/OpenAiCompatibleModelFactory.kt index 62500d173..38fcabcc9 100644 --- a/embabel-agent-openai/src/main/kotlin/com/embabel/agent/openai/OpenAiCompatibleModelFactory.kt +++ b/embabel-agent-openai/src/main/kotlin/com/embabel/agent/openai/OpenAiCompatibleModelFactory.kt @@ -15,6 +15,7 @@ */ package com.embabel.agent.openai +import com.embabel.agent.api.models.AtlasCloudModels import com.embabel.agent.api.models.DeepSeekModels import com.embabel.agent.api.models.GoogleGenAiModels import com.embabel.agent.api.models.MistralAiModels @@ -127,6 +128,18 @@ open class OpenAiCompatibleModelFactory( GoogleGenAiModels.PROVIDER, ) + /** + * Returns a [ByokSpec] for Atlas Cloud (OpenAI-compatible endpoint). + * Validates against [AtlasCloudModels.QWEN3_5_FLASH] by default. + */ + fun atlasCloud(apiKey: String): ByokSpec = + ByokSpec( + "https://api.atlascloud.ai/v1", + apiKey, + AtlasCloudModels.QWEN3_5_FLASH, + AtlasCloudModels.PROVIDER, + ) + /** * Returns a [ByokSpec] for a custom OpenAI-compatible provider. * @@ -191,8 +204,9 @@ open class OpenAiCompatibleModelFactory( * so it can be passed directly to [com.embabel.common.byok.detectProvider]. * * Obtained via the companion factory methods ([openAi], [deepSeek], [mistral], [gemini], - * or [byok] for custom providers). Use [validating] to override the default validation - * model and provider — for example if the key only grants access to a specific model tier. + * [atlasCloud], or [byok] for custom providers). Use [validating] to override the default + * validation model and provider — for example if the key only grants access to a specific + * model tier. */ class ByokSpec internal constructor( private val baseUrl: String?, diff --git a/embabel-agent-openai/src/test/kotlin/com/embabel/agent/openai/OpenAiCompatibleModelFactoryBuildValidatedTest.kt b/embabel-agent-openai/src/test/kotlin/com/embabel/agent/openai/OpenAiCompatibleModelFactoryBuildValidatedTest.kt index 948400041..eeaade590 100644 --- a/embabel-agent-openai/src/test/kotlin/com/embabel/agent/openai/OpenAiCompatibleModelFactoryBuildValidatedTest.kt +++ b/embabel-agent-openai/src/test/kotlin/com/embabel/agent/openai/OpenAiCompatibleModelFactoryBuildValidatedTest.kt @@ -157,6 +157,7 @@ class OpenAiCompatibleModelFactoryBuildValidatedTest { OpenAiCompatibleModelFactory.deepSeek(""), OpenAiCompatibleModelFactory.mistral("\t"), OpenAiCompatibleModelFactory.gemini(" "), + OpenAiCompatibleModelFactory.atlasCloud("\n"), ).forEach { spec -> val e = assertThrows { spec.buildValidated() } assertEquals(BLANK_API_KEY_MESSAGE, e.message) diff --git a/embabel-agent-openai/src/test/kotlin/com/embabel/agent/openai/OpenAiCompatibleModelFactoryByokIT.kt b/embabel-agent-openai/src/test/kotlin/com/embabel/agent/openai/OpenAiCompatibleModelFactoryByokIT.kt index f13ea8e7f..d76b72039 100644 --- a/embabel-agent-openai/src/test/kotlin/com/embabel/agent/openai/OpenAiCompatibleModelFactoryByokIT.kt +++ b/embabel-agent-openai/src/test/kotlin/com/embabel/agent/openai/OpenAiCompatibleModelFactoryByokIT.kt @@ -60,6 +60,15 @@ class OpenAiCompatibleModelFactoryByokIT { assertNotNull(service) } + @Test + @EnabledIfEnvironmentVariable(named = "ATLASCLOUD_API_KEY", matches = ".+") + fun `atlasCloud buildValidated succeeds with valid key`() { + val service = OpenAiCompatibleModelFactory.atlasCloud(System.getenv("ATLASCLOUD_API_KEY")) + .buildValidated() + assertNotNull(service) + assertEquals("Atlas Cloud", service.provider) + } + @Test @EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") fun `openAiEmbedding buildValidated succeeds with valid key`() { diff --git a/embabel-agent-openai/src/test/kotlin/com/embabel/agent/openai/OpenAiCompatibleModelFactoryByokSpecTest.kt b/embabel-agent-openai/src/test/kotlin/com/embabel/agent/openai/OpenAiCompatibleModelFactoryByokSpecTest.kt index a8e36e2e1..b50e1316d 100644 --- a/embabel-agent-openai/src/test/kotlin/com/embabel/agent/openai/OpenAiCompatibleModelFactoryByokSpecTest.kt +++ b/embabel-agent-openai/src/test/kotlin/com/embabel/agent/openai/OpenAiCompatibleModelFactoryByokSpecTest.kt @@ -15,6 +15,7 @@ */ package com.embabel.agent.openai +import com.embabel.agent.api.models.AtlasCloudModels import com.embabel.agent.api.models.DeepSeekModels import com.embabel.agent.api.models.GoogleGenAiModels import com.embabel.agent.api.models.MistralAiModels @@ -45,6 +46,11 @@ class OpenAiCompatibleModelFactoryByokSpecTest { assertInstanceOf(ByokFactory::class.java, OpenAiCompatibleModelFactory.gemini("key")) } + @Test + fun `atlasCloud returns ByokFactory`() { + assertInstanceOf(ByokFactory::class.java, OpenAiCompatibleModelFactory.atlasCloud("key")) + } + @Test fun `byok with explicit params returns ByokFactory`() { assertInstanceOf( @@ -74,11 +80,16 @@ class OpenAiCompatibleModelFactoryByokSpecTest { val deepSeekSpec = OpenAiCompatibleModelFactory.deepSeek("key") val mistralSpec = OpenAiCompatibleModelFactory.mistral("key") val geminiSpec = OpenAiCompatibleModelFactory.gemini("key") + val atlasCloudSpec = OpenAiCompatibleModelFactory.atlasCloud("key") // Each spec should construct without error (no network call at this stage) assertInstanceOf(ByokFactory::class.java, openAiSpec) assertInstanceOf(ByokFactory::class.java, deepSeekSpec) assertInstanceOf(ByokFactory::class.java, mistralSpec) assertInstanceOf(ByokFactory::class.java, geminiSpec) + assertInstanceOf(ByokFactory::class.java, atlasCloudSpec) + + val overridden = atlasCloudSpec.validating(AtlasCloudModels.QWEN3_8_MAX, AtlasCloudModels.PROVIDER) + assertInstanceOf(ByokFactory::class.java, overridden) } }