Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .justfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ itest *args:
jbang *args:
PATH="build/install/jbang/bin:$PATH" jbang {{args}}

jbangdebug *args:
JBANG_JAVA_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=1044 PATH="build/install/jbang/bin:$PATH" jbang {{args}}

java *args:
java {{args}}

Expand Down
135 changes: 134 additions & 1 deletion docs/modules/ROOT/pages/templates.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,137 @@ Templates can refer to properties passed in using `-Dkey=value` on the `jbang in

Any key specified via `-Dkey=value` becomes available as `\{key}` inside the templates.

It is recommended you write templates where there is some kind of valid default or fallback on behavior as users might not be aware there is need for a key.
It is recommended you write templates where there is some kind of valid default or fallback on behavior as users might not be aware there is need for a key.

== AI-Powered Code Generation

JBang supports 100% optional AI-powered code generation when using the `init` command. When you pass parameters to `jbang init`, JBang will attempt to use an AI provider to generate code based on your description.

The simplest way to get started is to set an API key environment variable and run:

[source,bash]
----
$ export OPENAI_API_KEY="your-api-key"
$ jbang init hello.java "create a simple hello world with command line arguments"
----

JBang will automatically detect and use your API key. The generated code will be inserted into the template using the `magiccontent` property.

=== Trying Different Models

If you want to try a different model, you can override just the model name:

[source,bash]
----
$ jbang init --ai-model=gpt-4o hello.java "create a simple hello world with command line arguments"
----

This works with any of the configuration methods below.

=== Advanced Configuration

For power users who need more control, JBang provides multiple ways to configure AI providers, with the following priority order (highest to lowest):

1. **Command-line options**: Pass options directly to the `init` command
2. **Environment variables**: Set environment variables in your shell
3. **Config properties**: Set persistent configuration using `jbang config set`
4. **Auto-detection**: JBang will automatically detect provider based on common API key/token environment variables if available

==== Command-Line Options

You can specify AI provider settings directly on the command line:

[source,bash]
----
$ jbang init --ai-provider=openai --ai-api-key=your-key --ai-model=gpt-4o-mini myapp.java "create a REST API client"
----

Available options:
* `--ai-provider`: Provider to use (e.g., `openai`, `openrouter`)
* `--ai-api-key`: API key/token for the AI provider
* `--ai-endpoint`: OpenAI-compatible endpoint URL
* `--ai-model`: Model name to use

==== Environment Variables

You can configure AI providers using environment variables:

* `JBANG_AI_PROVIDER`: Provider name
* `JBANG_AI_API_KEY`: API key/token
* `JBANG_AI_ENDPOINT`: API endpoint URL
* `JBANG_AI_MODEL`: Model name

[source,bash]
----
export JBANG_AI_API_KEY="your-api-key"
export JBANG_AI_ENDPOINT="https://api.openai.com/v1"
export JBANG_AI_MODEL="gpt-4o-mini"
jbang init myapp.java "create a REST API client"
----

==== Configuration Properties

You can set AI provider defaults using JBang's configuration system:

[source,bash]
----
$ jbang config set default.ai.provider openai

Copy link
Copy Markdown
Contributor

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:

defaultValue = "${JBANG_AI_PROVIDER:-${default.ai.provider)}"

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 be app.init.aiprovider, but in this case that's not very interesting/useful)

$ jbang config set default.ai.api-key your-api-key
$ jbang config set default.ai.endpoint https://api.openai.com/v1
$ jbang config set default.ai.model gpt-4o-mini
----

These settings will persist and apply to all `jbang init` commands unless overridden by command-line options.

==== Auto-Detection

If you don't configure anything explicitly, JBang will automatically detect and use common API keys from your environment.

**Important**: If you explicitly set a provider (via `--ai-provider`, `JBANG_AI_PROVIDER`, or config), that provider will be used regardless of which API keys are detected. This ensures predictable behavior when you want to use a specific provider.

When auto-detecting, JBang checks providers in the following order (first match wins):

[cols="1,2,3,3"]
|===
|Provider Name |Environment Variable |Endpoint |Default Model

|`openai`
|`OPENAI_API_KEY`
|`https://api.openai.com/v1`
|`gpt-5.1`

|`openrouter`
|`OPENROUTER_API_KEY`
|`https://openrouter.ai/api/v1`
|`x-ai/grok-code-fast-1`

|`google`
|`GEMINI_API_KEY`
|`https://generativelanguage.googleapis.com/v1beta/openai`
|`gemini-3-pro-preview`

|`opencode`
|`OPENCODE_API_KEY`
|`https://opencode.ai/zen/v1`
|`gpt-4`

|`github`
|`GITHUB_TOKEN`
|`https://models.github.ai/inference`
|`openai/gpt-4.1`

|`ollama` (explicit only)
|N/A (no auth required)
|`http://localhost:11434/v1`
|`llama3`
|===

NOTE: The `github` provider is checked last because `GITHUB_TOKEN` is commonly used for other GitHub operations and may cause false positives.

NOTE: The `ollama` provider is not auto-detected and must be explicitly set using `--ai-provider=ollama` or `JBANG_AI_PROVIDER=ollama`. It connects to a local Ollama instance and does not require an API key.

If you have any of these environment variables set, JBang will automatically use them without requiring any additional configuration.

If no AI provider API key is found, JBang will fall back to normal template initialization without AI generation.

NOTE: AI-generated code results can vary greatly. Sometimes it works perfectly, other times it may require adjustments or serve as inspiration.
32 changes: 32 additions & 0 deletions src/main/java/dev/jbang/ai/AIProvider.java
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();
}
158 changes: 158 additions & 0 deletions src/main/java/dev/jbang/ai/AIProviderFactory.java
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);
}
}
}
47 changes: 47 additions & 0 deletions src/main/java/dev/jbang/ai/ChatResponse.java
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 + ")";
}
}
}
Loading