// SPDX-FileCopyrightText: Amolith // // SPDX-License-Identifier: AGPL-3.0-or-later import gleam/dynamic/decode import gleam/http/request import gleam/httpc import gleam/int import gleam/json import gleam/list import gleam/option.{type Option, None, Some} import gleam/result const providers_url = "https://catwalk.charm.sh/v2/providers" pub type Provider { Provider( id: String, provider_type: String, api_key_env: Option(String), api_endpoint: Option(String), models: List(Model), ) } pub type Model { Model( id: String, can_reason: Bool, reasoning_levels: List(String), default_reasoning_effort: Option(String), ) } pub type ValidationError { FetchError(String) ProviderNotFound(String) ModelNotFound(provider: String, model: String) } pub fn validate( provider_id: String, model_id: String, ) -> Result(Nil, ValidationError) { use providers <- result.try(fetch_providers()) use provider <- result.try(find_provider(providers, provider_id)) find_model(provider, model_id) } pub fn get_provider(provider_id: String) -> Result(Provider, ValidationError) { use providers <- result.try(fetch_providers()) find_provider(providers, provider_id) } fn fetch_providers() -> Result(List(Provider), ValidationError) { let assert Ok(req) = request.to(providers_url) case httpc.send(req) { Ok(resp) if resp.status == 200 -> parse_providers(resp.body) Ok(resp) -> Error(FetchError("HTTP " <> int.to_string(resp.status))) Error(_) -> Error(FetchError("Network error")) } } fn parse_providers(body: String) -> Result(List(Provider), ValidationError) { let model_decoder = { use id <- decode.field("id", decode.string) use can_reason <- decode.field("can_reason", decode.bool) use reasoning_levels <- decode.optional_field( "reasoning_levels", [], decode.list(decode.string), ) use default_reasoning_effort <- decode.optional_field( "default_reasoning_effort", None, decode.string |> decode.map(Some), ) decode.success(Model( id:, can_reason:, reasoning_levels:, default_reasoning_effort:, )) } let provider_decoder = { use id <- decode.field("id", decode.string) use provider_type <- decode.field("type", decode.string) use api_key_env <- decode.optional_field( "api_key", None, decode.string |> decode.map(Some), ) use api_endpoint <- decode.optional_field( "api_endpoint", None, decode.string |> decode.map(Some), ) use models <- decode.field("models", decode.list(model_decoder)) decode.success(Provider( id:, provider_type:, api_key_env:, api_endpoint:, models:, )) } json.parse(body, decode.list(provider_decoder)) |> result.map_error(fn(_) { FetchError("Invalid JSON") }) } fn find_provider( providers: List(Provider), provider_id: String, ) -> Result(Provider, ValidationError) { providers |> list.find(fn(p) { p.id == provider_id }) |> result.map_error(fn(_) { ProviderNotFound(provider_id) }) } fn find_model( provider: Provider, model_id: String, ) -> Result(Nil, ValidationError) { provider.models |> list.find(fn(m) { m.id == model_id }) |> result.map(fn(_) { Nil }) |> result.map_error(fn(_) { ModelNotFound(provider.id, model_id) }) } /// Get a model by ID from a provider pub fn get_model( provider: Provider, model_id: String, ) -> Result(Model, ValidationError) { provider.models |> list.find(fn(m) { m.id == model_id }) |> result.map_error(fn(_) { ModelNotFound(provider.id, model_id) }) } /// Resolve an environment variable reference like "$OPENAI_API_KEY" to just "OPENAI_API_KEY" pub fn resolve_env_var_name(value: String) -> Option(String) { case value { "$" <> rest -> Some(rest) _ -> None } }