Skip to content

Conversation

@ericcurtin
Copy link
Contributor

@ericcurtin ericcurtin commented Oct 16, 2025

Add external OpenAI endpoint support

Add comprehensive tests for new server connection flags

Summary by Sourcery

Add server connection flags and external OpenAI endpoint support across CLI commands, implement URL resolution and context override logic, update client routing for external endpoints, and include comprehensive tests for these new capabilities

New Features:

  • Add --host, --port, --url, and --url-alias flags to pull, push, run, and list commands
  • Support external OpenAI-compatible endpoints via custom URLs and named presets (llamacpp, ollama, openrouter)

Enhancements:

  • Implement resolveServerURL and override functions to configure model runner context based on flags
  • Extend ModelRunnerContext and desktop client to route requests differently for external vs. internal endpoints
  • Update listModels and chat client logic to handle external OpenAI paths and output formats

Tests:

  • Add command-level tests for server connection flags on run, list, pull, and push
  • Add unit tests for resolveServerURL, backend validation, and API key retrieval
  • Add tests for prettyPrintOpenAIModels to validate table output formatting

Add external OpenAI endpoint support

Add comprehensive tests for new server connection flags

Signed-off-by: Eric Curtin <[email protected]>
Copilot AI review requested due to automatic review settings October 16, 2025 14:00
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Oct 16, 2025

Reviewer's Guide

This PR adds comprehensive support for custom server endpoints by introducing new connection flags (--host, --port, --url, --url-alias) across pull, push, run, and list commands. It centralizes endpoint resolution via resolveServerURL, conditionally overrides the model-runner context for local Docker or external OpenAI-compatible servers, enhances the listModels logic for varied OpenAI output formats, and updates the desktop client to route requests correctly. A full suite of tests validates flag registration, URL resolution, and output formatting.

Sequence diagram for overriding model runner context based on connection flags

sequenceDiagram
    actor User
    participant CLI
    participant "resolveServerURL()"
    participant "overrideModelRunnerContextFromURL()"
    participant "overrideModelRunnerContext()"
    User->>CLI: Run command with connection flags
    CLI->>resolveServerURL(): Parse flags
    resolveServerURL-->>CLI: Return serverURL, useOpenAI, apiKey
    CLI->>overrideModelRunnerContextFromURL(): If serverURL set
    overrideModelRunnerContextFromURL-->>CLI: Update context
    CLI->>overrideModelRunnerContext(): Else if host/port set
    overrideModelRunnerContext-->>CLI: Update context
Loading

Class diagram for updated ModelRunnerContext and ServerPreset types

classDiagram
    class ModelRunnerContext {
        +kind ModelRunnerEngineKind
        +urlPrefix url.URL
        +client DockerHttpClient
        +externalOpenAI bool
        +IsExternalOpenAI() bool
    }
    class ServerPreset {
        +Name string
        +URL string
    }
    ModelRunnerContext -- DockerHttpClient
    ModelRunnerContext -- url.URL
    ServerPreset "*" -- "1" ValidBackends : presets
Loading

Class diagram for updated desktop.Client request routing

classDiagram
    class Client {
        +List()
        +ListOpenAI(backend, apiKey)
        +ChatWithContext(ctx, backend, model, prompt, apiKey)
        -modelRunner ModelRunnerContext
    }
    Client -- ModelRunnerContext
    Client : ListOpenAI() uses IsExternalOpenAI() to select route
    Client : ChatWithContext() uses IsExternalOpenAI() to select route
Loading

Flow diagram for server endpoint resolution via resolveServerURL

flowchart TD
    A["User provides flags: --host, --port, --url, --url-alias"] --> B["resolveServerURL()"]
    B --> C{"Which flags are set?"}
    C -->|--url| D["Set serverURL = customURL, useOpenAI = true"]
    C -->|--url-alias| E["Lookup preset, set serverURL, useOpenAI = true"]
    C -->|--host/--port| F["Set serverURL = http://host:port, useOpenAI = false"]
    D --> G["Return serverURL, useOpenAI, apiKey"]
    E --> G
    F --> G
Loading

File-Level Changes

Change Details Files
Add server connection flags to CLI commands
  • Register --host, --port, --url, and --url-alias flags in run, list, pull, and push definitions
  • Integrate flag handling into RunE functions to override model-runner context
cmd/cli/commands/run.go
cmd/cli/commands/list.go
cmd/cli/commands/pull.go
cmd/cli/commands/push.go
Implement server URL resolution and context override utilities
  • Create resolveServerURL to validate and derive server URL, mode, and API key
  • Add overrideModelRunnerContext and overrideModelRunnerContextFromURL to swap contexts
cmd/cli/commands/backend.go
cmd/cli/commands/utils.go
Extend ModelRunnerContext and desktop client for external OpenAI endpoints
  • Add externalOpenAI flag and new URL/host context constructors
  • Modify ListOpenAI and ChatWithContext to select routes based on externalOpenAI
cmd/cli/desktop/context.go
cmd/cli/desktop/desktop.go
Enhance listModels for flexible OpenAI output
  • Adjust listModels to choose between JSON, quiet, and pretty table formats
  • Implement prettyPrintOpenAIModels helper for table rendering
cmd/cli/commands/list.go
Add tests for connection flags and backend utilities
  • Validate presence and types of new flags in run/list/pull/push tests
  • Cover resolveServerURL, validateBackend, ensureAPIKey scenarios
  • Test prettyPrintOpenAIModels output formatting
cmd/cli/commands/run_test.go
cmd/cli/commands/backend_test.go
cmd/cli/commands/list_test.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @ericcurtin, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the flexibility of the CLI by allowing users to specify custom server endpoints for interacting with model runners. It introduces new flags for host, port, and URL configuration, along with aliases for popular OpenAI-compatible services. This change streamlines the process of connecting to various local and remote inference servers, making the tool more adaptable to diverse deployment scenarios.

Highlights

  • New Server Connection Flags: Introduced --host, --port, --url, and --url-alias flags to pull, push, run, and list commands, allowing users to specify custom server endpoints for model operations.
  • External OpenAI Endpoint Support: Added support for connecting to external OpenAI-compatible API endpoints, including predefined aliases for llamacpp, ollama, and openrouter.
  • Dynamic Server URL Resolution: Implemented a new resolveServerURL utility function to intelligently determine the target server URL based on the provided flags, handling potential conflicts and API key requirements.
  • Enhanced Model Listing Output: The list command now offers improved output formatting for OpenAI-compatible models, including a new prettyPrintOpenAIModels function for a more readable table view.
  • Comprehensive Testing: New test files (backend_test.go, list_test.go) and additional tests in run_test.go have been added to ensure the robustness and correctness of the new server connection logic and output formats.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • There’s a lot of repeated flag‐lookup code in the new server flag tests; consider extracting a helper to reduce duplication and improve readability.
  • The resolveServerURL function is handling several distinct flows in one place—splitting out URL alias resolution, host/port defaults, and conflict checks into smaller helpers could make it easier to follow and maintain.
  • overrideModelRunnerContext and overrideModelRunnerContextFromURL share very similar context‐switch logic; consolidating them (or sharing a common setup path) would cut down on duplicated code.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- There’s a lot of repeated flag‐lookup code in the new server flag tests; consider extracting a helper to reduce duplication and improve readability.
- The resolveServerURL function is handling several distinct flows in one place—splitting out URL alias resolution, host/port defaults, and conflict checks into smaller helpers could make it easier to follow and maintain.
- overrideModelRunnerContext and overrideModelRunnerContextFromURL share very similar context‐switch logic; consolidating them (or sharing a common setup path) would cut down on duplicated code.

## Individual Comments

### Comment 1
<location> `cmd/cli/commands/backend_test.go:151` </location>
<code_context>
+	}
+}
+
+func TestValidateBackend(t *testing.T) {
+	tests := []struct {
+		name    string
</code_context>

<issue_to_address>
**nitpick (testing):** Tests for validateBackend do not check error message content.

Add assertions to verify that error messages are clear and match expected content.
</issue_to_address>

### Comment 2
<location> `cmd/cli/commands/list_test.go:11-20` </location>
<code_context>
+func TestPrettyPrintOpenAIModels(t *testing.T) {
</code_context>

<issue_to_address>
**suggestion (testing):** Tests for prettyPrintOpenAIModels do not cover models with missing or zero Created timestamps.

Add a test for models with a zero or missing Created field to verify correct handling and output formatting.

Suggested implementation:

```golang
	modelList := dmrm.OpenAIModelList{
		Object: "list",
		Data: []*dmrm.OpenAIModel{
			{
				ID:      "llama3.2:3b",
				Object:  "model",
				Created: time.Now().Unix() - 3600, // 1 hour ago
				OwnedBy: "docker",
			},
			{
				ID:      "missing-created",
				Object:  "model",
				Created: 0, // Zero timestamp
				OwnedBy: "docker",
			},
			// If the struct allows omitting Created, you could add:
			// {
			// 	ID:      "no-created-field",
			// 	Object:  "model",
			// 	OwnedBy: "docker",
			// },

```

```golang
	output := prettyPrintOpenAIModels(modelList)

	if !strings.Contains(output, "llama3.2:3b") {
		t.Errorf("Expected output to contain model ID llama3.2:3b")
	}
	if !strings.Contains(output, "missing-created") {
		t.Errorf("Expected output to contain model ID missing-created")
	}
	if !strings.Contains(output, "N/A") && !strings.Contains(output, "missing-created") {
		t.Errorf("Expected output to show 'N/A' or similar for zero/missing Created timestamp")
	}
	// If you add a model with no Created field, check for its output as well.

```

If the `dmrm.OpenAIModel` struct does not allow omitting the `Created` field (i.e., it's not a pointer or omitempty), you can only test with zero value. If it does, add a model without the `Created` field and update the test accordingly.

You may need to adjust the output check depending on how `prettyPrintOpenAIModels` formats models with zero or missing timestamps (e.g., "N/A", "Unknown", or blank).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

}
}

func TestValidateBackend(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (testing): Tests for validateBackend do not check error message content.

Add assertions to verify that error messages are clear and match expected content.

Comment on lines +11 to +20
func TestPrettyPrintOpenAIModels(t *testing.T) {
modelList := dmrm.OpenAIModelList{
Object: "list",
Data: []*dmrm.OpenAIModel{
{
ID: "llama3.2:3b",
Object: "model",
Created: time.Now().Unix() - 3600, // 1 hour ago
OwnedBy: "docker",
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Tests for prettyPrintOpenAIModels do not cover models with missing or zero Created timestamps.

Add a test for models with a zero or missing Created field to verify correct handling and output formatting.

Suggested implementation:

	modelList := dmrm.OpenAIModelList{
		Object: "list",
		Data: []*dmrm.OpenAIModel{
			{
				ID:      "llama3.2:3b",
				Object:  "model",
				Created: time.Now().Unix() - 3600, // 1 hour ago
				OwnedBy: "docker",
			},
			{
				ID:      "missing-created",
				Object:  "model",
				Created: 0, // Zero timestamp
				OwnedBy: "docker",
			},
			// If the struct allows omitting Created, you could add:
			// {
			// 	ID:      "no-created-field",
			// 	Object:  "model",
			// 	OwnedBy: "docker",
			// },
	output := prettyPrintOpenAIModels(modelList)

	if !strings.Contains(output, "llama3.2:3b") {
		t.Errorf("Expected output to contain model ID llama3.2:3b")
	}
	if !strings.Contains(output, "missing-created") {
		t.Errorf("Expected output to contain model ID missing-created")
	}
	if !strings.Contains(output, "N/A") && !strings.Contains(output, "missing-created") {
		t.Errorf("Expected output to show 'N/A' or similar for zero/missing Created timestamp")
	}
	// If you add a model with no Created field, check for its output as well.

If the dmrm.OpenAIModel struct does not allow omitting the Created field (i.e., it's not a pointer or omitempty), you can only test with zero value. If it does, add a model without the Created field and update the test accordingly.

You may need to adjust the output check depending on how prettyPrintOpenAIModels formats models with zero or missing timestamps (e.g., "N/A", "Unknown", or blank).

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds support for external OpenAI-compatible endpoints by introducing host/port/url/url-alias flags to pull, push, run, and list commands. The changes enable users to connect to custom model servers beyond the default Docker Model Runner.

Key Changes:

  • Added server connection flags (--host, --port, --url, --url-alias) to CLI commands
  • Implemented OpenAI endpoint detection and path routing logic
  • Added comprehensive test coverage for new flag functionality

Reviewed Changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
cmd/cli/desktop/desktop.go Modified ListOpenAI and ChatWithContext to route requests differently for external OpenAI endpoints
cmd/cli/desktop/context.go Added context creation functions for custom host/port/URL with external OpenAI support
cmd/cli/commands/utils.go Added helper functions to override model runner context with custom connection parameters
cmd/cli/commands/run.go Integrated server connection flags and URL resolution into run command
cmd/cli/commands/push.go Added host/port flags to push command
cmd/cli/commands/pull.go Added host/port flags to pull command
cmd/cli/commands/list.go Added server connection flags and OpenAI model list formatting support
cmd/cli/commands/backend.go Implemented URL resolution logic and server preset configurations
cmd/cli/commands/run_test.go Added tests verifying presence and types of new server connection flags
cmd/cli/commands/list_test.go Added tests for OpenAI model list formatting functionality
cmd/cli/commands/backend_test.go Added comprehensive tests for URL resolution, backend validation, and API key handling

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

return "", errors.New("OPENAI_API_KEY environment variable is required when using --backend=openai")
if apiKey != "" {
return apiKey, nil
}
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ensureAPIKey function no longer returns an error when the API key is missing for OpenAI backend. This is a breaking change from the original behavior where it returned an error message 'OPENAI_API_KEY environment variable is required when using --backend=openai'. This change may cause issues for code that expects an error to be returned when the API key is missing.

Suggested change
}
}
return "", fmt.Errorf("OPENAI_API_KEY environment variable is required when using --backend=openai")

Copilot uses AI. Check for mistakes.
Comment on lines +92 to +100
break
}
}
if !found {
return "", false, "", fmt.Errorf("invalid url-alias '%s'. Valid options are: llamacpp, ollama, openrouter", urlAlias)
}

apiKey = os.Getenv("OPENAI_API_KEY")
} else if hostPortSpecified {
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing validation for required API key when using openrouter url-alias. The openrouter preset requires an API key to function, but there's no check to ensure it exists before returning. Consider adding validation similar to the original ensureAPIKey behavior for openrouter specifically.

Suggested change
break
}
}
if !found {
return "", false, "", fmt.Errorf("invalid url-alias '%s'. Valid options are: llamacpp, ollama, openrouter", urlAlias)
}
apiKey = os.Getenv("OPENAI_API_KEY")
} else if hostPortSpecified {
// Check for required API key for openrouter
apiKey = os.Getenv("OPENAI_API_KEY")
if preset.Name == "openrouter" && apiKey == "" {
return "", false, "", fmt.Errorf("the openrouter preset requires an API key. Please set the OPENAI_API_KEY environment variable.")
}
break
}
}
if !found {
return "", false, "", fmt.Errorf("invalid url-alias '%s'. Valid options are: llamacpp, ollama, openrouter", urlAlias)
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces valuable functionality by adding flags to support external OpenAI-compatible endpoints for several commands. The implementation is well-structured and includes a comprehensive set of tests. My review highlights a critical issue with a failing test, a high-severity bug where a required API key is not enforced, and a few medium-severity suggestions to improve code maintainability and address a discrepancy between the code and the pull request description. Overall, these are great additions, and addressing the feedback will enhance the robustness of the new features.

Comment on lines +205 to +208
name: "openai backend without key",
backend: "openai",
wantErr: true,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This test case expects ensureAPIKey to return an error when the openai backend is used without an API key. However, the implementation of ensureAPIKey in cmd/cli/commands/backend.go was changed to no longer return an error in this scenario (it returns "", nil). This test is now incorrect and will fail. The test should be updated to reflect the new behavior of ensureAPIKey.

Suggested change
name: "openai backend without key",
backend: "openai",
wantErr: true,
},
{
name: "openai backend without key",
backend: "openai",
wantErr: false,
},

return "", false, "", fmt.Errorf("invalid url-alias '%s'. Valid options are: llamacpp, ollama, openrouter", urlAlias)
}

apiKey = os.Getenv("OPENAI_API_KEY")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The logic to enforce OPENAI_API_KEY for the openrouter alias is missing. The function currently retrieves the API key but doesn't return an error if it's missing for openrouter, which is a required behavior. This will cause the openrouter url-alias without API key test in backend_test.go to fail.

apiKey = os.Getenv("OPENAI_API_KEY")
		if urlAlias == "openrouter" && apiKey == "" {
			return "", false, "", errors.New("OPENAI_API_KEY environment variable is required when using --url-alias=openrouter")
		}

Comment on lines +95 to +97
if !found {
return "", false, "", fmt.Errorf("invalid url-alias '%s'. Valid options are: llamacpp, ollama, openrouter", urlAlias)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error message for an invalid url-alias hardcodes the list of valid options. This can become out of sync if the ServerPresets slice is updated. For better maintainability, this list should be generated dynamically from ServerPresets.

		if !found {
			validAliases := make([]string, len(ServerPresets))
			for i, p := range ServerPresets {
				validAliases[i] = p.Name
			}
			return "", false, "", fmt.Errorf("invalid url-alias '%s'. Valid options are: %s", urlAlias, strings.Join(validAliases, ", "))
		}

Comment on lines +49 to +50
c.Flags().StringVar(&host, "host", "", "Host address to bind Docker Model Runner (default \"127.0.0.1\")")
c.Flags().IntVar(&port, "port", 0, "Docker container port for Docker Model Runner (default: 12434)")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The pull command (and push command) only receives the --host and --port flags. However, the pull request description states that --url and --url-alias flags should also be added to these commands. This is a discrepancy between the implementation and the description. If pull and push are only meant to work with the local model runner, then the PR description should be updated to reflect this.

Comment on lines +188 to +193
// Use HTTP default client for custom host/port
client := http.DefaultClient

if userAgent := os.Getenv("USER_AGENT"); userAgent != "" {
setUserAgent(client, userAgent)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The code for creating a default HTTP client and setting the user agent is duplicated here and in NewContextWithURLExternal (lines 216-221). This could be extracted into a private helper function to reduce duplication and improve maintainability.

For example:

func newHTTPClientWithUserAgent() DockerHttpClient {
	client := http.DefaultClient
	if userAgent := os.Getenv("USER_AGENT"); userAgent != "" {
		setUserAgent(client, userAgent)
	}
	return client
}

Then both NewContextWithHostPort and NewContextWithURLExternal can use client := newHTTPClientWithUserAgent().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants