Skip to content

Commit 065d078

Browse files
Add OpenAI-compatible provider support
Introduce support for OpenAI-compatible providers (Provider = "OpenAICompatible"). Updates include: docs (README, HOW_IT_WORKS, TECHNICAL_REFERENCE, CONTRIBUTING), docker files (.env.example, docker-compose.yml), CLI help, and code changes to plumb new config fields (BaseUrl, Organization, Project, ModelId mapping) and environment variables (OPENAI_COMPAT_*). KernelFactory, AgentKernelFactory, and Executor config/validation logic were extended to detect the new provider, validate required fields, and create chat completions against a custom endpoint when OpenAICompatible is selected. DockerRunner now propagates OPENAI_COMPAT_* env vars into containers. Note: appsettings.json in this changeset contains a populated ApiKey value — do not commit real secrets; remove or rotate this key and use environment variables instead.
1 parent 3ab13be commit 065d078

File tree

13 files changed

+325
-40
lines changed

13 files changed

+325
-40
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ dotnet build
5858

5959
```bash
6060
cp docker/.env.example docker/.env
61-
# Edit docker/.env and set OPENAI_API_KEY or Azure OpenAI credentials
61+
# Edit docker/.env and set OpenAI, OpenAI-compatible, or Azure OpenAI credentials
6262
```
6363

6464
**5. Run tests**

HOW_IT_WORKS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ The Analyst's job is to read the tutorial and produce a machine-readable test pl
6060

6161
**Scraping** — The Analyst fetches the tutorial URL and parses the HTML. It follows navigation links within the same tutorial series, collecting up to a configurable maximum number of pages. Each page is converted to clean Markdown.
6262

63-
**Analysis** — The Analyst sends the Markdown content to an AI model (OpenAI or Azure OpenAI) with a prompt that instructs it to extract every action a developer must take. The result is a list of structured steps in JSON format, called the **test plan** (`testplan.json`).
63+
**Analysis** — The Analyst sends the Markdown content to an AI model (OpenAI, Azure OpenAI, or any OpenAI-compatible provider) with a prompt that instructs it to extract every action a developer must take. The result is a list of structured steps in JSON format, called the **test plan** (`testplan.json`).
6464

6565
**Compaction** — Long tutorials can produce hundreds of raw steps. To keep execution time and AI cost reasonable, the Analyst merges adjacent steps of the same type (e.g., two consecutive file edits become one step with two modifications). This is controlled by the `--target-steps` and `--max-steps` arguments.
6666

README.md

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
# TutorialValidator
22

3-
[![CI](https://github.com/AbpFramework/TutorialValidator/actions/workflows/ci.yml/badge.svg)](https://github.com/AbpFramework/TutorialValidator/actions/workflows/ci.yml)
43
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
54
[![.NET](https://img.shields.io/badge/.NET-10-512BD4)](https://dotnet.microsoft.com/download/dotnet/10.0)
65

7-
TutorialValidator is an AI-powered tool that checks whether a software documentation tutorial actually works. You give it a URL, it scrapes the tutorial, turns every instruction into an executable step, then runs those steps exactly as a developer would — installing packages, writing files, running commands, making HTTP calls, and asserting results. If any step fails, the tutorial has a bug.
6+
TutorialValidator is an AI-powered tool that checks whether a software documentation tutorial actually works. You give it a URL, it scrapes the tutorial, turns every instruction into an executable step, then runs those steps exactly as a developer would — installing packages, writing files, running commands, making HTTP calls, and asserting results.
87

9-
It was built to validate [ABP Framework](https://abp.io) tutorials, but the architecture supports any publicly accessible tutorial.
8+
We originally built it internally to validate [ABP Framework](https://abp.io) tutorials, and then decided to publish it as open source so you can use it to validate any publicly accessible tutorial.
109

1110
---
1211

@@ -18,7 +17,7 @@ Before you start, make sure you have the following installed:
1817
|---|---|---|
1918
| .NET SDK | 10.0 | [dotnet.microsoft.com](https://dotnet.microsoft.com/download/dotnet/10.0) |
2019
| Docker Desktop | Latest | [docker.com/get-started](https://www.docker.com/get-started/) |
21-
| OpenAI **or** Azure OpenAI API key || [platform.openai.com](https://platform.openai.com) or your Azure portal |
20+
| AI provider API key || Refer to your AI provider's documentation |
2221

2322
> Docker is required for the default (recommended) execution mode. If you want to run without Docker, see [Running Locally Without Docker](#running-locally-without-docker) below.
2423
@@ -43,22 +42,28 @@ cp docker/.env.example docker/.env
4342

4443
**Step 3 — Add your API key**
4544

46-
Open `docker/.env` in any text editor and fill in your credentials.
45+
Open `docker/.env` in any text editor and fill in your AI provider credentials. For example:
4746

48-
For OpenAI:
4947
```env
48+
# OpenAI
5049
OPENAI_API_KEY=sk-...
5150
OPENAI_MODEL=gpt-5.2
52-
```
5351
54-
For Azure OpenAI:
55-
```env
52+
# OpenAI-Compatible (works with providers that expose an OpenAI-compatible API)
53+
# OPENAI_COMPAT_BASE_URL=https://your-provider.example.com/v1
54+
# OPENAI_COMPAT_API_KEY=your-key
55+
# OPENAI_COMPAT_MODEL=gpt-4o-mini
56+
# AI_PROVIDER=OpenAICompatible
57+
58+
# Azure OpenAI
5659
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
5760
AZURE_OPENAI_API_KEY=your-key
5861
AZURE_OPENAI_DEPLOYMENT=gpt-4o
5962
AI_PROVIDER=AzureOpenAI
6063
```
6164

65+
See [Environment Variables](#environment-variables) for all supported providers.
66+
6267
**Step 4 — Run the validation**
6368

6469
```bash
@@ -210,10 +215,11 @@ The file at `src/Validator.Orchestrator/appsettings.json` controls all default s
210215

211216
| Field | Description |
212217
|---|---|
213-
| `Provider` | AI provider to use. Accepted values: `OpenAI`, `AzureOpenAI`. Auto-detected from environment variables if omitted. |
214-
| `Model` | The model name to request from OpenAI (e.g. `gpt-5.2`, `gpt-4o`). Ignored when using Azure OpenAI. |
218+
| `Provider` | AI provider to use. Accepted values: `OpenAI`, `AzureOpenAI`, `OpenAICompatible`. Auto-detected from environment variables if omitted. |
219+
| `Model` | The model name to request from your AI provider (e.g. `gpt-5.2`, `gpt-4o`). Ignored when using Azure OpenAI. |
215220
| `DeploymentName` | The deployment name for Azure OpenAI. When using OpenAI directly, this can mirror the `Model` value or be left empty. |
216-
| `ApiKey` | Your API key. Leave blank and use the `OPENAI_API_KEY` or `AZURE_OPENAI_API_KEY` environment variable instead — do not commit keys to source control. |
221+
| `ApiKey` | Your AI provider API key. Leave blank and use environment variables (`OPENAI_API_KEY`, `AZURE_OPENAI_API_KEY`, or `OPENAI_COMPAT_API_KEY`) instead — do not commit keys to source control. |
222+
| `BaseUrl` | Base URL for OpenAI-compatible providers (e.g. `https://your-provider.example.com/v1`). Used when `Provider` is `OpenAICompatible`. |
217223

218224
#### Docker section
219225

@@ -339,10 +345,15 @@ Environment variables always override values in `appsettings.json`.
339345
|---|---|
340346
| `OPENAI_API_KEY` | OpenAI API key. |
341347
| `OPENAI_MODEL` | OpenAI model name (e.g. `gpt-5.2`, `gpt-4o`). |
348+
| `OPENAI_COMPAT_BASE_URL` | Base URL for an OpenAI-compatible API endpoint. |
349+
| `OPENAI_COMPAT_API_KEY` | API key for an OpenAI-compatible provider. |
350+
| `OPENAI_COMPAT_MODEL` | Model name for OpenAI-compatible providers. |
351+
| `OPENAI_COMPAT_ORG` | Optional organization ID for OpenAI-compatible providers. |
352+
| `OPENAI_COMPAT_PROJECT` | Optional project ID for OpenAI-compatible providers. |
342353
| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI endpoint URL (e.g. `https://your-resource.openai.azure.com/`). |
343354
| `AZURE_OPENAI_API_KEY` | Azure OpenAI API key. |
344355
| `AZURE_OPENAI_DEPLOYMENT` | Azure OpenAI deployment name. |
345-
| `AI_PROVIDER` | Force a specific provider: `OpenAI` or `AzureOpenAI`. Auto-detected if omitted. |
356+
| `AI_PROVIDER` | Force a specific provider: `OpenAI`, `AzureOpenAI`, or `OpenAICompatible`. Auto-detected if omitted. |
346357
| `Discord__Enabled` | Enable Discord notifications: `true` or `false`. |
347358
| `Discord__WebhookUrl` | Discord incoming webhook URL. |
348359
| `EXECUTOR_BUILD_GATE_INTERVAL` | Senior persona only: run `dotnet build` every N steps as a sanity check. `0` disables this. |
@@ -366,5 +377,5 @@ Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) for de
366377

367378
## License
368379

369-
MIT License — Copyright (c) 2024 [Volosoft](https://volosoft.com).
380+
MIT License — Copyright (c) [Volosoft](https://volosoft.com).
370381
See [LICENSE](LICENSE) for the full text.

TECHNICAL_REFERENCE.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ After compaction, all steps are renumbered sequentially starting from 1.
146146
### 3.1 Initialization
147147

148148
`AgentKernelFactory.CreateExecutorKernel` builds a `Microsoft.SemanticKernel.Kernel` with:
149-
- The configured AI chat completion service (OpenAI or Azure OpenAI)
149+
- The configured AI chat completion service (OpenAI, Azure OpenAI, or OpenAI-compatible endpoint)
150150
- Four plugins registered as kernel functions (see [3.3 Plugins](#33-plugins))
151151
- A `FunctionCallTracker` that intercepts and records every function call and its result for deterministic result parsing
152152

@@ -487,7 +487,8 @@ A workaround for the .NET 10 SDK image ships an RC/preview runtime while ABP CLI
487487

488488
1. If `AI_PROVIDER` is set explicitly, use that value (`OpenAI` or `AzureOpenAI`).
489489
2. If `AZURE_OPENAI_ENDPOINT` is set, default to `AzureOpenAI`.
490-
3. Otherwise, use `OpenAI`.
490+
3. Otherwise, if `OPENAI_COMPAT_BASE_URL` and `OPENAI_COMPAT_API_KEY` are present, use `OpenAICompatible`.
491+
4. Otherwise, use `OpenAI`.
491492

492493
### OpenAI Configuration
493494

@@ -506,6 +507,19 @@ Required:
506507

507508
`AI.Model` is ignored for Azure OpenAI; the model is determined by the deployment.
508509

510+
### OpenAI-Compatible Configuration
511+
512+
Required:
513+
- `OPENAI_COMPAT_BASE_URL` or `AI.BaseUrl` in `appsettings.json`
514+
- `OPENAI_COMPAT_API_KEY` or `AI.ApiKey`
515+
- `OPENAI_COMPAT_MODEL` or `AI.ModelId` (falls back to `AI.DeploymentName`)
516+
517+
Optional:
518+
- `OPENAI_COMPAT_ORG` / `AI.Organization`
519+
- `OPENAI_COMPAT_PROJECT` / `AI.Project`
520+
521+
Set `AI_PROVIDER=OpenAICompatible` to force this mode when multiple provider variables are present.
522+
509523
### Configuration Precedence
510524

511525
Environment variables always override `appsettings.json`. The configuration is loaded using `Microsoft.Extensions.Configuration.IConfigurationBuilder`:

docker/.env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,21 @@ MSSQL_SA_PASSWORD=YourStrong!Password123
1010
OPENAI_API_KEY=your-openai-api-key-here
1111
OPENAI_MODEL=gpt-4o
1212

13+
# Option 1b: OpenAI-compatible provider
14+
# OPENAI_COMPAT_BASE_URL=https://your-provider.example.com/v1
15+
# OPENAI_COMPAT_API_KEY=your-openai-compatible-api-key-here
16+
# OPENAI_COMPAT_MODEL=gpt-4o-mini
17+
# OPENAI_COMPAT_ORG=optional-org-id
18+
# OPENAI_COMPAT_PROJECT=optional-project-id
19+
1320
# Option 2: Azure OpenAI
1421
# AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
1522
# AZURE_OPENAI_API_KEY=your-azure-openai-api-key-here
1623
# AZURE_OPENAI_DEPLOYMENT=gpt-4o
1724

1825
# AI Provider (auto-detected if not set)
1926
# AI_PROVIDER=OpenAI
27+
# AI_PROVIDER=OpenAICompatible
2028

2129
# Discord notifications (optional)
2230
Discord__Enabled=false

docker/docker-compose.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ services:
4444
- AZURE_OPENAI_DEPLOYMENT=${AZURE_OPENAI_DEPLOYMENT:-gpt-4o}
4545
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
4646
- OPENAI_MODEL=${OPENAI_MODEL:-gpt-4o}
47+
- OPENAI_COMPAT_BASE_URL=${OPENAI_COMPAT_BASE_URL:-}
48+
- OPENAI_COMPAT_API_KEY=${OPENAI_COMPAT_API_KEY:-}
49+
- OPENAI_COMPAT_MODEL=${OPENAI_COMPAT_MODEL:-gpt-4o}
50+
- OPENAI_COMPAT_ORG=${OPENAI_COMPAT_ORG:-}
51+
- OPENAI_COMPAT_PROJECT=${OPENAI_COMPAT_PROJECT:-}
4752
- AI_PROVIDER=${AI_PROVIDER:-}
4853
# Database connection string for SQL Server
4954
- ConnectionStrings__Default=Server=sqlserver;Database=TutorialValidatorTest;User=sa;Password=YourStrong!Password123;TrustServerCertificate=true;Encrypt=false

src/Validator.Analyst/Analysis/AIConfiguration.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace Validator.Analyst.Analysis;
66
public class AIConfiguration
77
{
88
/// <summary>
9-
/// AI provider type: "AzureOpenAI" or "OpenAI".
9+
/// AI provider type: "AzureOpenAI", "OpenAI", or "OpenAICompatible".
1010
/// </summary>
1111
public string Provider { get; set; } = "AzureOpenAI";
1212

@@ -29,4 +29,19 @@ public class AIConfiguration
2929
/// Model ID to use (defaults to DeploymentName if not specified).
3030
/// </summary>
3131
public string? ModelId { get; set; }
32+
33+
/// <summary>
34+
/// Base URL for OpenAI-compatible providers.
35+
/// </summary>
36+
public string? BaseUrl { get; set; }
37+
38+
/// <summary>
39+
/// Optional organization identifier for OpenAI-compatible providers.
40+
/// </summary>
41+
public string? Organization { get; set; }
42+
43+
/// <summary>
44+
/// Optional project identifier for OpenAI-compatible providers.
45+
/// </summary>
46+
public string? Project { get; set; }
3247
}

src/Validator.Analyst/Analysis/KernelFactory.cs

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@ namespace Validator.Analyst.Analysis;
88
/// </summary>
99
public static class KernelFactory
1010
{
11+
private const string ProviderAzureOpenAI = "AzureOpenAI";
12+
private const string ProviderOpenAI = "OpenAI";
13+
private const string ProviderOpenAICompatible = "OpenAICompatible";
14+
1115
/// <summary>
1216
/// Creates a Kernel instance from configuration.
1317
/// </summary>
1418
public static Kernel CreateFromConfiguration(AIConfiguration config)
1519
{
1620
var builder = Kernel.CreateBuilder();
1721

18-
if (config.Provider.Equals("AzureOpenAI", StringComparison.OrdinalIgnoreCase))
22+
if (config.Provider.Equals(ProviderAzureOpenAI, StringComparison.OrdinalIgnoreCase))
1923
{
2024
if (string.IsNullOrWhiteSpace(config.Endpoint))
2125
throw new InvalidOperationException("Azure OpenAI endpoint is required");
@@ -28,7 +32,7 @@ public static Kernel CreateFromConfiguration(AIConfiguration config)
2832
apiKey: config.ApiKey,
2933
modelId: config.ModelId);
3034
}
31-
else if (config.Provider.Equals("OpenAI", StringComparison.OrdinalIgnoreCase))
35+
else if (config.Provider.Equals(ProviderOpenAI, StringComparison.OrdinalIgnoreCase))
3236
{
3337
if (string.IsNullOrWhiteSpace(config.ApiKey))
3438
throw new InvalidOperationException("OpenAI API key is required");
@@ -37,9 +41,24 @@ public static Kernel CreateFromConfiguration(AIConfiguration config)
3741
modelId: config.ModelId ?? config.DeploymentName,
3842
apiKey: config.ApiKey);
3943
}
44+
else if (config.Provider.Equals(ProviderOpenAICompatible, StringComparison.OrdinalIgnoreCase))
45+
{
46+
if (string.IsNullOrWhiteSpace(config.ApiKey))
47+
throw new InvalidOperationException("OpenAI-compatible API key is required");
48+
if (string.IsNullOrWhiteSpace(config.BaseUrl))
49+
throw new InvalidOperationException("OpenAI-compatible base URL is required");
50+
51+
var endpoint = new Uri(config.BaseUrl, UriKind.Absolute);
52+
53+
builder.AddOpenAIChatCompletion(
54+
modelId: config.ModelId ?? config.DeploymentName,
55+
apiKey: config.ApiKey,
56+
endpoint: endpoint);
57+
}
4058
else
4159
{
42-
throw new InvalidOperationException($"Unknown AI provider: {config.Provider}");
60+
throw new InvalidOperationException(
61+
$"Unknown AI provider: {config.Provider}. Supported providers: {ProviderAzureOpenAI}, {ProviderOpenAI}, {ProviderOpenAICompatible}.");
4362
}
4463

4564
return builder.Build();
@@ -98,16 +117,27 @@ public static AIConfiguration LoadConfiguration(string? configPath = null)
98117
// Try to bind from "AI" section first
99118
configuration.GetSection("AI").Bind(aiConfig);
100119

120+
// Support existing AI:Model field by mapping it to ModelId when present.
121+
var configuredModel = configuration["AI:Model"];
122+
if (!string.IsNullOrEmpty(configuredModel) && string.IsNullOrEmpty(aiConfig.ModelId))
123+
{
124+
aiConfig.ModelId = configuredModel;
125+
}
126+
101127
// Override with environment variables if present
128+
var explicitProvider = false;
102129
var provider = Environment.GetEnvironmentVariable("AI_PROVIDER");
103130
if (!string.IsNullOrEmpty(provider))
131+
{
104132
aiConfig.Provider = provider;
133+
explicitProvider = true;
134+
}
105135

106136
// Azure OpenAI environment variables
107137
var azureEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
108-
if (!string.IsNullOrEmpty(azureEndpoint))
138+
if (!string.IsNullOrEmpty(azureEndpoint) && !explicitProvider)
109139
{
110-
aiConfig.Provider = "AzureOpenAI";
140+
aiConfig.Provider = ProviderAzureOpenAI;
111141
aiConfig.Endpoint = azureEndpoint;
112142
}
113143

@@ -119,18 +149,54 @@ public static AIConfiguration LoadConfiguration(string? configPath = null)
119149
if (!string.IsNullOrEmpty(azureDeployment))
120150
aiConfig.DeploymentName = azureDeployment;
121151

152+
// OpenAI-compatible environment variables
153+
var openAiCompatBaseUrl = Environment.GetEnvironmentVariable("OPENAI_COMPAT_BASE_URL");
154+
var openAiCompatApiKey = Environment.GetEnvironmentVariable("OPENAI_COMPAT_API_KEY");
155+
var openAiCompatModel = Environment.GetEnvironmentVariable("OPENAI_COMPAT_MODEL");
156+
var openAiCompatOrg = Environment.GetEnvironmentVariable("OPENAI_COMPAT_ORG");
157+
var openAiCompatProject = Environment.GetEnvironmentVariable("OPENAI_COMPAT_PROJECT");
158+
159+
if (!string.IsNullOrEmpty(openAiCompatBaseUrl))
160+
aiConfig.BaseUrl = openAiCompatBaseUrl;
161+
162+
if (!string.IsNullOrEmpty(openAiCompatOrg))
163+
aiConfig.Organization = openAiCompatOrg;
164+
165+
if (!string.IsNullOrEmpty(openAiCompatProject))
166+
aiConfig.Project = openAiCompatProject;
167+
168+
if (!string.IsNullOrEmpty(openAiCompatApiKey))
169+
aiConfig.ApiKey = openAiCompatApiKey;
170+
171+
if (!string.IsNullOrEmpty(openAiCompatModel))
172+
aiConfig.ModelId = openAiCompatModel;
173+
174+
if (!explicitProvider && !string.IsNullOrEmpty(openAiCompatBaseUrl) && !string.IsNullOrEmpty(openAiCompatApiKey))
175+
{
176+
aiConfig.Provider = ProviderOpenAICompatible;
177+
}
178+
122179
// OpenAI environment variables
123180
var openaiApiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY");
124181
if (!string.IsNullOrEmpty(openaiApiKey) && string.IsNullOrEmpty(aiConfig.ApiKey))
125182
{
126-
aiConfig.Provider = "OpenAI";
183+
if (!explicitProvider)
184+
{
185+
aiConfig.Provider = ProviderOpenAI;
186+
}
127187
aiConfig.ApiKey = openaiApiKey;
128188
}
129189

130190
var openaiModel = Environment.GetEnvironmentVariable("OPENAI_MODEL");
131191
if (!string.IsNullOrEmpty(openaiModel))
132192
aiConfig.ModelId = openaiModel;
133193

194+
if (aiConfig.Provider.Equals(ProviderOpenAICompatible, StringComparison.OrdinalIgnoreCase) &&
195+
string.IsNullOrWhiteSpace(aiConfig.ModelId))
196+
{
197+
aiConfig.ModelId = aiConfig.DeploymentName;
198+
}
199+
134200
return aiConfig;
135201
}
136202

@@ -142,16 +208,33 @@ public static void ValidateConfiguration(AIConfiguration config)
142208
if (string.IsNullOrWhiteSpace(config.ApiKey))
143209
{
144210
throw new InvalidOperationException(
145-
"AI API key is required. Set AZURE_OPENAI_API_KEY or OPENAI_API_KEY environment variable, " +
211+
"AI API key is required. Set AZURE_OPENAI_API_KEY, OPENAI_API_KEY, or OPENAI_COMPAT_API_KEY environment variable, " +
146212
"or configure in appsettings.json under AI:ApiKey");
147213
}
148214

149-
if (config.Provider.Equals("AzureOpenAI", StringComparison.OrdinalIgnoreCase) &&
215+
if (config.Provider.Equals(ProviderAzureOpenAI, StringComparison.OrdinalIgnoreCase) &&
150216
string.IsNullOrWhiteSpace(config.Endpoint))
151217
{
152218
throw new InvalidOperationException(
153219
"Azure OpenAI endpoint is required. Set AZURE_OPENAI_ENDPOINT environment variable, " +
154220
"or configure in appsettings.json under AI:Endpoint");
155221
}
222+
223+
if (config.Provider.Equals(ProviderOpenAICompatible, StringComparison.OrdinalIgnoreCase))
224+
{
225+
if (string.IsNullOrWhiteSpace(config.BaseUrl))
226+
{
227+
throw new InvalidOperationException(
228+
"OpenAI-compatible base URL is required. Set OPENAI_COMPAT_BASE_URL environment variable, " +
229+
"or configure in appsettings.json under AI:BaseUrl");
230+
}
231+
232+
if (string.IsNullOrWhiteSpace(config.ModelId) && string.IsNullOrWhiteSpace(config.DeploymentName))
233+
{
234+
throw new InvalidOperationException(
235+
"OpenAI-compatible model is required. Set OPENAI_COMPAT_MODEL environment variable, " +
236+
"or configure in appsettings.json under AI:ModelId or AI:DeploymentName");
237+
}
238+
}
156239
}
157240
}

0 commit comments

Comments
 (0)