001/*
002 * Copyright (c) 2026 Singular
003 * SPDX-License-Identifier: MIT
004 */
005
006package ai.singlr.openai;
007
008import ai.singlr.core.common.Strings;
009import ai.singlr.core.model.Model;
010import ai.singlr.core.model.ModelConfig;
011import ai.singlr.core.model.ModelProvider;
012
013/**
014 * ModelProvider implementation for OpenAI's Responses API.
015 *
016 * <p>Supports canonical OpenAI models (GPT-5.4, GPT-4.1, GPT-4o, o3, o4-mini) out of the box. When
017 * {@link ModelConfig#baseUrl()} is set — pointing at Azure OpenAI, an OpenAI-compatible proxy
018 * (LiteLLM, vLLM, Ollama), or Vertex AI — any non-blank {@code modelId} is accepted. The string is
019 * used verbatim as the {@code model} field in the request body, which Azure OpenAI maps to the
020 * deployment name. Context-window and max-output-tokens metadata default to {@code 0} ("unknown")
021 * for unrecognised ids; callers can override output tokens via {@link
022 * ModelConfig.Builder#withMaxOutputTokens(Integer)}.
023 */
024public class OpenAIProvider implements ModelProvider {
025
026  private static final String PROVIDER_NAME = "openai";
027
028  @Override
029  public String name() {
030    return PROVIDER_NAME;
031  }
032
033  @Override
034  public Model create(String modelId, ModelConfig config) {
035    var known = OpenAIModelId.fromId(modelId);
036    if (known != null) {
037      return new OpenAIModel(known, config);
038    }
039    if (!Strings.isBlank(config.baseUrl())) {
040      return new OpenAIModel(modelId, config);
041    }
042    throw new IllegalArgumentException(
043        "Unsupported model: "
044            + modelId
045            + ". Set ModelConfig.baseUrl for custom endpoints (Azure, proxy, Vertex).");
046  }
047
048  @Override
049  public boolean supports(String modelId) {
050    return OpenAIModelId.isSupported(modelId);
051  }
052}