Repository navigation
feat: JBangGPT -> JBang AI with multiple providers and no-preview #2324
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package dev.jbang.ai; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| /** | ||
| * Base interface for AI providers that can generate code using | ||
| * OpenAI-compatible chat completions API. | ||
| */ | ||
| public interface AIProvider { | ||
|
|
||
| /** | ||
| * Generate code using the AI provider. | ||
| * | ||
| * @param baseName The base name for the generated class | ||
| * @param extension The file extension (e.g., "java", "kt") | ||
| * @param request The user's request/description | ||
| * @param javaVersion The requested Java version (e.g., "17+", "21") or null if | ||
| * not specified | ||
| * @return The generated code | ||
| * @throws IOException if there's a problem communicating with the API | ||
| */ | ||
| String generateCode(String baseName, String extension, String request, String javaVersion) throws IOException; | ||
|
|
||
| /** | ||
| * Get the name of this provider (e.g., "OpenAI", "OpenRouter") | ||
| * | ||
| * @return The provider name | ||
| */ | ||
| String getName(); | ||
|
|
||
| String getModel(); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| package dev.jbang.ai; | ||
|
|
||
| import static java.lang.System.getenv; | ||
|
|
||
| import java.util.LinkedHashMap; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.function.Supplier; | ||
|
|
||
| import dev.jbang.util.Util; | ||
|
|
||
| /** | ||
| * Factory for creating AI providers based on available API keys. | ||
| * | ||
| * <p> | ||
| * Supports 15+ AI providers with automatic detection from environment | ||
| * variables. When a provider is explicitly set, it takes priority over | ||
| * auto-detection. Provider configurations are initialized once at class load | ||
| * time for optimal performance. | ||
| * </p> | ||
| */ | ||
| public class AIProviderFactory { | ||
|
|
||
| private static final Map<String, ProviderConfig> PROVIDER_MAP = createProviderMap(); | ||
|
|
||
| private String defaultProvider; | ||
| private String defaultKey; | ||
| private String defaultEndpoint; | ||
| private String defaultModel; | ||
|
|
||
| public AIProviderFactory() { | ||
| this(null, null, null, null); | ||
| } | ||
|
|
||
| public AIProviderFactory(String defaultProvider, String defaultKey, String defaultEndpoint, String defaultModel) { | ||
| this.defaultProvider = defaultProvider; | ||
| this.defaultKey = defaultKey; | ||
| this.defaultEndpoint = defaultEndpoint; | ||
| this.defaultModel = defaultModel; | ||
| } | ||
|
|
||
| private static Map<String, ProviderConfig> createProviderMap() { | ||
| Map<String, ProviderConfig> map = new LinkedHashMap<>(); | ||
| // Build provider configurations in order (LinkedHashMap maintains insertion | ||
| // order) | ||
| // order below is more or less arbitrary as those are all fairly unique keys | ||
| map.put("openai", new ProviderConfig("OPENAI_API_KEY", "openai", "https://api.openai.com/v1", | ||
| "gpt-5.1")); | ||
| map.put("openrouter", new ProviderConfig("OPENROUTER_API_KEY", "openrouter", | ||
| "https://openrouter.ai/api/v1", "x-ai/grok-code-fast-1")); | ||
| // map.put("anthropic", new ProviderConfig("CLAUDE_API_KEY", "anthropic", | ||
| // "https://api.anthropic.com/v1", "claude-3.5-haiku")); | ||
| map.put("google", new ProviderConfig("GEMINI_API_KEY", "google", | ||
| "https://generativelanguage.googleapis.com/v1beta/openai", "gemini-3-pro-preview")); | ||
| // map.put("groq", new ProviderConfig("GROQ_API_KEY", "groq", | ||
| // "https://api.groq.com/openai/v1", | ||
| // "llama-3.1-70b-versatile")); | ||
| // map.put("together", new ProviderConfig("TOGETHER_API_KEY", "together", | ||
| // "https://api.together.xyz/v1", "meta-llama/Llama-3-70b-chat-hf")); | ||
| // map.put("mistral", new ProviderConfig("MISTRAL_API_KEY", "mistral", | ||
| // "https://api.mistral.ai/v1", | ||
| // "mistral-large-latest")); | ||
| // map.put("deepinfra", new ProviderConfig("DEEPINFRA_API_KEY", "deepinfra", | ||
| // "https://api.deepinfra.com/v1/openai", "meta-llama/Llama-3-70b-chat-hf")); | ||
| map.put("opencode", new ProviderConfig("OPENCODE_API_KEY", "opencode", | ||
| "https://opencode.ai/zen/v1", "gpt-5.1")); | ||
| // map.put("fireworks", new ProviderConfig("FIREWORKS_API_KEY", "fireworks", | ||
| // "https://api.fireworks.ai/inference/v1", | ||
| // "accounts/fireworks/models/llama-v3-70b-instruct")); | ||
| // map.put("anyscale", new ProviderConfig("ANYSCALE_API_KEY", "anyscale", | ||
| // "https://api.endpoints.anyscale.com/v1", "meta-llama/Llama-3-70b-chat-hf")); | ||
| // map.put("huggingface", new ProviderConfig("HUGGINGFACE_API_KEY", | ||
| // "huggingface", | ||
| // "https://api-inference.huggingface.co/v1", | ||
| // "meta-llama/Llama-3-70b-chat-hf")); | ||
| // map.put("perplexity", new ProviderConfig("PERPLEXITY_API_KEY", "perplexity", | ||
| // "https://api.perplexity.ai", "llama-3-sonar-large-32k-online")); | ||
|
|
||
| // putting github last as GITHUB_TOKEN could be a personal access token or a PAT | ||
| // for many other things. | ||
| map.put("github", new ProviderConfig("GITHUB_TOKEN", "github", | ||
| "https://models.github.ai/inference", "openai/gpt-4.1")); | ||
| map.put("ollama", new ProviderConfig(() -> null, "ollama", "http://localhost:11434/v1", "llama3")); | ||
| return map; | ||
| } | ||
|
|
||
| public AIProvider createProvider(String provider, String key, String endpoint, String model) { | ||
| return new OpenAIProvider( | ||
| Objects.toString(defaultProvider, provider), | ||
| Objects.toString(defaultKey, key), | ||
| Objects.toString(defaultEndpoint, endpoint), | ||
| Objects.toString(defaultModel, model)); | ||
| } | ||
|
|
||
| /** | ||
| * Create an AI provider based on available environment variables. | ||
| * | ||
| * <p> | ||
| * If a provider is explicitly set (via defaultProvider), it will be used | ||
| * regardless of which API keys are detected. This ensures predictable behavior | ||
| * when a specific provider is requested. | ||
| * </p> | ||
| * | ||
| * <p> | ||
| * Otherwise, auto-detection checks providers in order (first match wins): | ||
| * OpenAI, OpenRouter, Google (Gemini), OpenCode Zen, GitHub (checked last due | ||
| * to potential token ambiguity), Ollama (only when explicitly set). | ||
| * </p> | ||
| * | ||
| * @return An AI provider if a valid API key is found, null otherwise | ||
| */ | ||
| public AIProvider createProvider() { | ||
| // If provider is explicitly set, use it | ||
| if (defaultProvider != null && !defaultProvider.trim().isEmpty()) { | ||
| Util.verboseMsg("Using explicitly set provider: " + defaultProvider); | ||
| ProviderConfig config = PROVIDER_MAP.get(defaultProvider.toLowerCase()); | ||
| if (config != null) { | ||
| AIProvider provider = config.createProvider(this); | ||
| if (provider != null) { | ||
| return provider; | ||
| } else { | ||
| Util.verboseMsg("No provider found for explicitly set provider: " + defaultProvider); | ||
| return null; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| for (ProviderConfig config : PROVIDER_MAP.values()) { | ||
| AIProvider provider = config.createProvider(this); | ||
| if (provider != null) { | ||
| return provider; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| private static class ProviderConfig { | ||
| private final Supplier<String> keySupplier; | ||
| private final String providerName; | ||
| private final String endpoint; | ||
| private final String model; | ||
|
|
||
| ProviderConfig(String envVar, String providerName, String endpoint, String model) { | ||
| this(() -> getenv(envVar), providerName, endpoint, model); | ||
| } | ||
|
|
||
| ProviderConfig(Supplier<String> keySupplier, String providerName, String endpoint, String model) { | ||
| this.keySupplier = keySupplier; | ||
| this.providerName = providerName; | ||
| this.endpoint = endpoint; | ||
| this.model = model; | ||
| } | ||
|
|
||
| AIProvider createProvider(AIProviderFactory factory) { | ||
| return factory.createProvider(providerName, keySupplier.get(), endpoint, model); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| package dev.jbang.ai; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * Response model for OpenAI-compatible chat completion APIs. | ||
| */ | ||
| public class ChatResponse { | ||
| public String id; | ||
| public String object; | ||
| public double created; | ||
| public String model; | ||
| public Usage usage; | ||
| public List<Choice> choices; | ||
|
|
||
| public static class Usage { | ||
| public Integer prompt_tokens; | ||
| public Integer completion_tokens; | ||
| public Integer total_tokens; | ||
| public Map<String, Object> prompt_tokens_details; | ||
| public Map<String, Object> completion_tokens_details; | ||
| } | ||
|
|
||
| public static class Choice { | ||
| public Message message; | ||
|
|
||
| public static class Message { | ||
| public String role; | ||
| public String content; | ||
| } | ||
| } | ||
|
|
||
| public Error error; | ||
|
|
||
| public static class Error { | ||
| String message; | ||
| String type; | ||
| String param; | ||
| String code; | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return type + ": " + message + " (code:" + code + "/param:" + param + ")"; | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Everything looks good, except for this, the default values don't start with "default.". THe fact that in the code you have to do:
is just an internal implementation detail.
Also, special chars are removed from the names. In this case you're using a
.in the name, which is not valid. So the actual name becomes:aiprovider. (Although the fqn can also be used, which would beapp.init.aiprovider, but in this case that's not very interesting/useful)