mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-13 14:19:47 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12e207dc3f | |||
| 00e74ab071 | |||
| 1653b8544b |
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||
"version": "1.79.2"
|
||||
"version": "1.80.0"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
English | [中文](./CHANGELOG.zh.md)
|
||||
|
||||
## 1.80.0 - 2026-03-24
|
||||
|
||||
### Features
|
||||
- `baoyu-image-gen`: add Azure OpenAI as independent image generation provider with flexible endpoint parsing, deployment-name resolution, quality mapping, and reference image validation
|
||||
|
||||
## 1.79.2 - 2026-03-23
|
||||
|
||||
### Fixes
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
[English](./CHANGELOG.md) | 中文
|
||||
|
||||
## 1.80.0 - 2026-03-24
|
||||
|
||||
### 新功能
|
||||
- `baoyu-image-gen`:新增 Azure OpenAI 作为独立图像生成服务商,支持灵活的端点解析、部署名称推断、质量映射及参考图片格式校验
|
||||
|
||||
## 1.79.2 - 2026-03-23
|
||||
|
||||
### 修复
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.79.2**.
|
||||
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.80.0**.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -31,7 +31,7 @@ Execute: `${BUN_X} skills/<skill>/scripts/main.ts [options]`
|
||||
|
||||
- **Bun**: TypeScript runtime (`bun` preferred, fallback `npx -y bun`)
|
||||
- **Chrome**: Required for CDP-based skills (gemini-web, post-to-x/wechat/weibo, url-to-markdown). All CDP skills share a single profile, override via `BAOYU_CHROME_PROFILE_DIR` env var. Platform paths: [docs/chrome-profile.md](docs/chrome-profile.md)
|
||||
- **Image generation APIs**: `baoyu-image-gen` requires API key (OpenAI, Google, OpenRouter, DashScope, or Replicate) configured in EXTEND.md
|
||||
- **Image generation APIs**: `baoyu-image-gen` requires API key (OpenAI, Azure OpenAI, Google, OpenRouter, DashScope, or Replicate) configured in EXTEND.md
|
||||
- **Gemini Web auth**: Browser cookies (first run opens Chrome for login, `--login` to refresh)
|
||||
|
||||
## Security
|
||||
|
||||
@@ -663,7 +663,7 @@ AI-powered generation backends.
|
||||
|
||||
#### baoyu-image-gen
|
||||
|
||||
AI SDK-based image generation using OpenAI, Google, OpenRouter, DashScope (Aliyun Tongyi Wanxiang), Jimeng (即梦), Seedream (豆包), and Replicate APIs. Supports text-to-image, reference images, aspect ratios, and quality presets.
|
||||
AI SDK-based image generation using OpenAI, Azure OpenAI, Google, OpenRouter, DashScope (Aliyun Tongyi Wanxiang), Jimeng (即梦), Seedream (豆包), and Replicate APIs. Supports text-to-image, reference images, aspect ratios, and quality presets.
|
||||
|
||||
```bash
|
||||
# Basic generation (auto-detect provider)
|
||||
@@ -678,6 +678,9 @@ AI SDK-based image generation using OpenAI, Google, OpenRouter, DashScope (Aliyu
|
||||
# Specific provider
|
||||
/baoyu-image-gen --prompt "A cat" --image cat.png --provider openai
|
||||
|
||||
# Azure OpenAI (model = deployment name)
|
||||
/baoyu-image-gen --prompt "A cat" --image cat.png --provider azure --model gpt-image-1.5
|
||||
|
||||
# OpenRouter
|
||||
/baoyu-image-gen --prompt "A cat" --image cat.png --provider openrouter
|
||||
|
||||
@@ -693,7 +696,7 @@ AI SDK-based image generation using OpenAI, Google, OpenRouter, DashScope (Aliyu
|
||||
# Seedream (豆包)
|
||||
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider seedream
|
||||
|
||||
# With reference images (Google, OpenAI, OpenRouter, Replicate, or Seedream 5.0/4.5/4.0)
|
||||
# With reference images (Google, OpenAI, Azure OpenAI, OpenRouter, Replicate, or Seedream 5.0/4.5/4.0)
|
||||
/baoyu-image-gen --prompt "Make it blue" --image out.png --ref source.png
|
||||
```
|
||||
|
||||
|
||||
+5
-2
@@ -663,7 +663,7 @@ AI 驱动的生成后端。
|
||||
|
||||
#### baoyu-image-gen
|
||||
|
||||
基于 AI SDK 的图像生成,支持 OpenAI、Google、OpenRouter、DashScope(阿里通义万相)、即梦(Jimeng)、豆包(Seedream)和 Replicate API。支持文生图、参考图、宽高比和质量预设。
|
||||
基于 AI SDK 的图像生成,支持 OpenAI、Azure OpenAI、Google、OpenRouter、DashScope(阿里通义万相)、即梦(Jimeng)、豆包(Seedream)和 Replicate API。支持文生图、参考图、宽高比和质量预设。
|
||||
|
||||
```bash
|
||||
# 基础生成(自动检测服务商)
|
||||
@@ -678,6 +678,9 @@ AI 驱动的生成后端。
|
||||
# 指定服务商
|
||||
/baoyu-image-gen --prompt "一只猫" --image cat.png --provider openai
|
||||
|
||||
# Azure OpenAI(model 为部署名称)
|
||||
/baoyu-image-gen --prompt "一只猫" --image cat.png --provider azure --model gpt-image-1.5
|
||||
|
||||
# OpenRouter
|
||||
/baoyu-image-gen --prompt "一只猫" --image cat.png --provider openrouter
|
||||
|
||||
@@ -693,7 +696,7 @@ AI 驱动的生成后端。
|
||||
# 豆包(Seedream)
|
||||
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider seedream
|
||||
|
||||
# 带参考图(Google、OpenAI、OpenRouter、Replicate 或 Seedream 5.0/4.5/4.0)
|
||||
# 带参考图(Google、OpenAI、Azure OpenAI、OpenRouter、Replicate 或 Seedream 5.0/4.5/4.0)
|
||||
/baoyu-image-gen --prompt "把它变成蓝色" --image out.png --ref source.png
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: baoyu-image-gen
|
||||
description: AI image generation with OpenAI, Google, OpenRouter, DashScope, Jimeng, Seedream and Replicate APIs. Supports text-to-image, reference images, aspect ratios, and batch generation from saved prompt files. Sequential by default; use batch parallel generation when the user already has multiple prompts or wants stable multi-image throughput. Use when user asks to generate, create, or draw images.
|
||||
description: AI image generation with OpenAI, Azure OpenAI, Google, OpenRouter, DashScope, Jimeng, Seedream and Replicate APIs. Supports text-to-image, reference images, aspect ratios, and batch generation from saved prompt files. Sequential by default; use batch parallel generation when the user already has multiple prompts or wants stable multi-image throughput. Use when user asks to generate, create, or draw images.
|
||||
version: 1.56.3
|
||||
metadata:
|
||||
openclaw:
|
||||
@@ -13,7 +13,7 @@ metadata:
|
||||
|
||||
# Image Generation (AI SDK)
|
||||
|
||||
Official API-based image generation. Supports OpenAI, Google, OpenRouter, DashScope (阿里通义万象), Jimeng (即梦), Seedream (豆包) and Replicate providers.
|
||||
Official API-based image generation. Supports OpenAI, Azure OpenAI, Google, OpenRouter, DashScope (阿里通义万象), Jimeng (即梦), Seedream (豆包) and Replicate providers.
|
||||
|
||||
## Script Directory
|
||||
|
||||
@@ -74,12 +74,15 @@ ${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --quality 2k
|
||||
# From prompt files
|
||||
${BUN_X} {baseDir}/scripts/main.ts --promptfiles system.md content.md --image out.png
|
||||
|
||||
# With reference images (Google, OpenAI, OpenRouter, Replicate, or Seedream 4.0/4.5/5.0)
|
||||
# With reference images (Google, OpenAI, Azure OpenAI, OpenRouter, Replicate, or Seedream 4.0/4.5/5.0)
|
||||
${BUN_X} {baseDir}/scripts/main.ts --prompt "Make blue" --image out.png --ref source.png
|
||||
|
||||
# With reference images (explicit provider/model)
|
||||
${BUN_X} {baseDir}/scripts/main.ts --prompt "Make blue" --image out.png --provider google --model gemini-3-pro-image-preview --ref source.png
|
||||
|
||||
# Azure OpenAI (model means deployment name)
|
||||
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --provider azure --model gpt-image-1.5
|
||||
|
||||
# OpenRouter (recommended default model)
|
||||
${BUN_X} {baseDir}/scripts/main.ts --prompt "A cat" --image out.png --provider openrouter
|
||||
|
||||
@@ -147,13 +150,13 @@ Paths in `promptFiles`, `image`, and `ref` are resolved relative to the batch fi
|
||||
| `--image <path>` | Output image path (required in single-image mode) |
|
||||
| `--batchfile <path>` | JSON batch file for multi-image generation |
|
||||
| `--jobs <count>` | Worker count for batch mode (default: auto, max from config, built-in default 10) |
|
||||
| `--provider google\|openai\|openrouter\|dashscope\|jimeng\|seedream\|replicate` | Force provider (default: auto-detect) |
|
||||
| `--model <id>`, `-m` | Model ID (Google: `gemini-3-pro-image-preview`; OpenAI: `gpt-image-1.5`; OpenRouter: `google/gemini-3.1-flash-image-preview`; DashScope: `qwen-image-2.0-pro`) |
|
||||
| `--provider google\|openai\|azure\|openrouter\|dashscope\|jimeng\|seedream\|replicate` | Force provider (default: auto-detect) |
|
||||
| `--model <id>`, `-m` | Model ID (Google: `gemini-3-pro-image-preview`; OpenAI: `gpt-image-1.5`; Azure: deployment name such as `gpt-image-1.5` or `image-prod`; OpenRouter: `google/gemini-3.1-flash-image-preview`; DashScope: `qwen-image-2.0-pro`) |
|
||||
| `--ar <ratio>` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
|
||||
| `--size <WxH>` | Size (e.g., `1024x1024`) |
|
||||
| `--quality normal\|2k` | Quality preset (default: `2k`) |
|
||||
| `--imageSize 1K\|2K\|4K` | Image size for Google/OpenRouter (default: from quality) |
|
||||
| `--ref <files...>` | Reference images. Supported by Google multimodal, OpenAI GPT Image edits, OpenRouter multimodal models, Replicate, and Seedream 5.0/4.5/4.0. Not supported by Jimeng, Seedream 3.0, or removed SeedEdit 3.0 |
|
||||
| `--ref <files...>` | Reference images. Supported by Google multimodal, OpenAI GPT Image edits, Azure OpenAI edits (PNG/JPG only), OpenRouter multimodal models, Replicate, and Seedream 5.0/4.5/4.0. Not supported by Jimeng, Seedream 3.0, or removed SeedEdit 3.0 |
|
||||
| `--n <count>` | Number of images |
|
||||
| `--json` | JSON output |
|
||||
|
||||
@@ -162,6 +165,7 @@ Paths in `promptFiles`, `image`, and `ref` are resolved relative to the batch fi
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `OPENAI_API_KEY` | OpenAI API key |
|
||||
| `AZURE_OPENAI_API_KEY` | Azure OpenAI API key |
|
||||
| `OPENROUTER_API_KEY` | OpenRouter API key |
|
||||
| `GOOGLE_API_KEY` | Google API key |
|
||||
| `DASHSCOPE_API_KEY` | DashScope API key (阿里云) |
|
||||
@@ -170,6 +174,8 @@ Paths in `promptFiles`, `image`, and `ref` are resolved relative to the batch fi
|
||||
| `JIMENG_SECRET_ACCESS_KEY` | Jimeng (即梦) Volcengine secret key |
|
||||
| `ARK_API_KEY` | Seedream (豆包) Volcengine ARK API key |
|
||||
| `OPENAI_IMAGE_MODEL` | OpenAI model override |
|
||||
| `AZURE_OPENAI_DEPLOYMENT` | Azure default deployment name |
|
||||
| `AZURE_OPENAI_IMAGE_MODEL` | Backward-compatible alias for Azure default deployment/model name |
|
||||
| `OPENROUTER_IMAGE_MODEL` | OpenRouter model override (default: `google/gemini-3.1-flash-image-preview`) |
|
||||
| `GOOGLE_IMAGE_MODEL` | Google model override |
|
||||
| `DASHSCOPE_IMAGE_MODEL` | DashScope model override (default: `qwen-image-2.0-pro`) |
|
||||
@@ -177,6 +183,8 @@ Paths in `promptFiles`, `image`, and `ref` are resolved relative to the batch fi
|
||||
| `JIMENG_IMAGE_MODEL` | Jimeng model override (default: jimeng_t2i_v40) |
|
||||
| `SEEDREAM_IMAGE_MODEL` | Seedream model override (default: doubao-seedream-5-0-260128) |
|
||||
| `OPENAI_BASE_URL` | Custom OpenAI endpoint |
|
||||
| `AZURE_OPENAI_BASE_URL` | Azure resource endpoint or deployment endpoint |
|
||||
| `AZURE_API_VERSION` | Azure image API version (default: `2025-04-01-preview`) |
|
||||
| `OPENROUTER_BASE_URL` | Custom OpenRouter endpoint (default: `https://openrouter.ai/api/v1`) |
|
||||
| `OPENROUTER_HTTP_REFERER` | Optional app/site URL for OpenRouter attribution |
|
||||
| `OPENROUTER_TITLE` | Optional app name for OpenRouter attribution |
|
||||
@@ -201,6 +209,8 @@ Model priority (highest → lowest), applies to all providers:
|
||||
3. Env var: `<PROVIDER>_IMAGE_MODEL` (e.g., `GOOGLE_IMAGE_MODEL`)
|
||||
4. Built-in default
|
||||
|
||||
For Azure, `--model` / `default_model.azure` should be the Azure deployment name. `AZURE_OPENAI_DEPLOYMENT` is the preferred env var, and `AZURE_OPENAI_IMAGE_MODEL` remains as a backward-compatible alias.
|
||||
|
||||
**EXTEND.md overrides env vars**. If both EXTEND.md `default_model.google: "gemini-3-pro-image-preview"` and env var `GOOGLE_IMAGE_MODEL=gemini-3.1-flash-image-preview` exist, EXTEND.md wins.
|
||||
|
||||
**Agent MUST display model info** before each generation:
|
||||
|
||||
@@ -47,6 +47,8 @@ options:
|
||||
description: "Gemini multimodal - high quality, reference images, flexible sizes"
|
||||
- label: "OpenAI"
|
||||
description: "GPT Image - consistent quality, reliable output"
|
||||
- label: "Azure OpenAI"
|
||||
description: "Azure-hosted GPT Image deployments with resource-specific routing"
|
||||
- label: "OpenRouter"
|
||||
description: "Router for Gemini/FLUX/OpenAI-compatible image models"
|
||||
- label: "DashScope"
|
||||
@@ -87,6 +89,20 @@ options:
|
||||
description: "Strong text-to-image quality through OpenRouter"
|
||||
```
|
||||
|
||||
### Question 2c: Default Azure Deployment
|
||||
|
||||
Only show if user selected Azure OpenAI.
|
||||
|
||||
```yaml
|
||||
header: "Azure Deploy"
|
||||
question: "Default Azure image deployment name?"
|
||||
options:
|
||||
- label: "gpt-image-1.5 (Recommended)"
|
||||
description: "Best default if your Azure deployment uses the same name"
|
||||
- label: "gpt-image-1"
|
||||
description: "Previous GPT Image deployment name"
|
||||
```
|
||||
|
||||
### Question 3: Default Quality
|
||||
|
||||
```yaml
|
||||
@@ -130,6 +146,7 @@ default_image_size: null
|
||||
default_model:
|
||||
google: [selected google model or null]
|
||||
openai: null
|
||||
azure: [selected azure deployment or null]
|
||||
openrouter: [selected openrouter model or null]
|
||||
dashscope: null
|
||||
replicate: null
|
||||
@@ -166,6 +183,23 @@ options:
|
||||
description: "Previous generation GPT Image model"
|
||||
```
|
||||
|
||||
### Azure Deployment Selection
|
||||
|
||||
```yaml
|
||||
header: "Azure Deploy"
|
||||
question: "Choose a default Azure image deployment name?"
|
||||
options:
|
||||
- label: "gpt-image-1.5 (Recommended)"
|
||||
description: "Use when your Azure deployment name matches the GPT-image-1.5 model"
|
||||
- label: "gpt-image-1"
|
||||
description: "Use when your Azure deployment name matches GPT-image-1"
|
||||
```
|
||||
|
||||
Notes for Azure setup:
|
||||
|
||||
- In `baoyu-image-gen`, Azure `--model` / `default_model.azure` should be the Azure deployment name, not just the underlying model family.
|
||||
- If the deployment name is custom, save that exact deployment name in `default_model.azure`.
|
||||
|
||||
### OpenRouter Model Selection
|
||||
|
||||
```yaml
|
||||
@@ -230,6 +264,7 @@ After user selects a model:
|
||||
default_model:
|
||||
google: [value or null]
|
||||
openai: [value or null]
|
||||
azure: [value or null]
|
||||
openrouter: [value or null]
|
||||
dashscope: [value or null]
|
||||
replicate: [value or null]
|
||||
|
||||
@@ -11,7 +11,7 @@ description: EXTEND.md YAML schema for baoyu-image-gen user preferences
|
||||
---
|
||||
version: 1
|
||||
|
||||
default_provider: null # google|openai|openrouter|dashscope|replicate|null (null = auto-detect)
|
||||
default_provider: null # google|openai|azure|openrouter|dashscope|replicate|null (null = auto-detect)
|
||||
|
||||
default_quality: null # normal|2k|null (null = use default: 2k)
|
||||
|
||||
@@ -22,6 +22,7 @@ default_image_size: null # 1K|2K|4K|null (Google/OpenRouter, overrides qualit
|
||||
default_model:
|
||||
google: null # e.g., "gemini-3-pro-image-preview", "gemini-3.1-flash-image-preview"
|
||||
openai: null # e.g., "gpt-image-1.5", "gpt-image-1"
|
||||
azure: null # Azure deployment name, e.g., "gpt-image-1.5" or "image-prod"
|
||||
openrouter: null # e.g., "google/gemini-3.1-flash-image-preview"
|
||||
dashscope: null # e.g., "qwen-image-2.0-pro"
|
||||
replicate: null # e.g., "google/nano-banana-pro"
|
||||
@@ -38,6 +39,9 @@ batch:
|
||||
openai:
|
||||
concurrency: 3
|
||||
start_interval_ms: 1100
|
||||
azure:
|
||||
concurrency: 3
|
||||
start_interval_ms: 1100
|
||||
openrouter:
|
||||
concurrency: 3
|
||||
start_interval_ms: 1100
|
||||
@@ -58,6 +62,7 @@ batch:
|
||||
| `default_image_size` | string\|null | null | Google/OpenRouter image size (overrides quality) |
|
||||
| `default_model.google` | string\|null | null | Google default model |
|
||||
| `default_model.openai` | string\|null | null | OpenAI default model |
|
||||
| `default_model.azure` | string\|null | null | Azure default deployment name |
|
||||
| `default_model.openrouter` | string\|null | null | OpenRouter default model |
|
||||
| `default_model.dashscope` | string\|null | null | DashScope default model |
|
||||
| `default_model.replicate` | string\|null | null | Replicate default model |
|
||||
@@ -87,6 +92,7 @@ default_image_size: 2K
|
||||
default_model:
|
||||
google: "gemini-3-pro-image-preview"
|
||||
openai: "gpt-image-1.5"
|
||||
azure: "gpt-image-1.5"
|
||||
openrouter: "google/gemini-3.1-flash-image-preview"
|
||||
dashscope: "qwen-image-2.0-pro"
|
||||
replicate: "google/nano-banana-pro"
|
||||
@@ -96,6 +102,9 @@ batch:
|
||||
replicate:
|
||||
concurrency: 5
|
||||
start_interval_ms: 700
|
||||
azure:
|
||||
concurrency: 3
|
||||
start_interval_ms: 1100
|
||||
openrouter:
|
||||
concurrency: 3
|
||||
start_interval_ms: 1100
|
||||
|
||||
@@ -123,6 +123,7 @@ default_image_size: 2K
|
||||
default_model:
|
||||
google: gemini-3-pro-image-preview
|
||||
openai: gpt-image-1.5
|
||||
azure: image-prod
|
||||
batch:
|
||||
max_workers: 8
|
||||
provider_limits:
|
||||
@@ -131,6 +132,9 @@ batch:
|
||||
start_interval_ms: 900
|
||||
openai:
|
||||
concurrency: 4
|
||||
azure:
|
||||
concurrency: 1
|
||||
start_interval_ms: 1500
|
||||
`;
|
||||
|
||||
const config = parseSimpleYaml(yaml);
|
||||
@@ -142,6 +146,7 @@ batch:
|
||||
assert.equal(config.default_image_size, "2K");
|
||||
assert.equal(config.default_model?.google, "gemini-3-pro-image-preview");
|
||||
assert.equal(config.default_model?.openai, "gpt-image-1.5");
|
||||
assert.equal(config.default_model?.azure, "image-prod");
|
||||
assert.equal(config.batch?.max_workers, 8);
|
||||
assert.deepEqual(config.batch?.provider_limits?.google, {
|
||||
concurrency: 2,
|
||||
@@ -150,6 +155,10 @@ batch:
|
||||
assert.deepEqual(config.batch?.provider_limits?.openai, {
|
||||
concurrency: 4,
|
||||
});
|
||||
assert.deepEqual(config.batch?.provider_limits?.azure, {
|
||||
concurrency: 1,
|
||||
start_interval_ms: 1500,
|
||||
});
|
||||
});
|
||||
|
||||
test("mergeConfig only fills values missing from CLI args", () => {
|
||||
@@ -203,6 +212,8 @@ test("detectProvider selects an available ref-capable provider for reference-ima
|
||||
useEnv(t, {
|
||||
GOOGLE_API_KEY: null,
|
||||
OPENAI_API_KEY: "openai-key",
|
||||
AZURE_OPENAI_API_KEY: null,
|
||||
AZURE_OPENAI_BASE_URL: null,
|
||||
OPENROUTER_API_KEY: null,
|
||||
DASHSCOPE_API_KEY: null,
|
||||
REPLICATE_API_TOKEN: null,
|
||||
@@ -216,6 +227,27 @@ test("detectProvider selects an available ref-capable provider for reference-ima
|
||||
);
|
||||
});
|
||||
|
||||
test("detectProvider selects Azure when only Azure credentials are configured", (t) => {
|
||||
useEnv(t, {
|
||||
GOOGLE_API_KEY: null,
|
||||
OPENAI_API_KEY: null,
|
||||
AZURE_OPENAI_API_KEY: "azure-key",
|
||||
AZURE_OPENAI_BASE_URL: "https://example.openai.azure.com",
|
||||
OPENROUTER_API_KEY: null,
|
||||
DASHSCOPE_API_KEY: null,
|
||||
REPLICATE_API_TOKEN: null,
|
||||
JIMENG_ACCESS_KEY_ID: null,
|
||||
JIMENG_SECRET_ACCESS_KEY: null,
|
||||
ARK_API_KEY: null,
|
||||
});
|
||||
|
||||
assert.equal(detectProvider(makeArgs()), "azure");
|
||||
assert.equal(
|
||||
detectProvider(makeArgs({ referenceImages: ["ref.png"] })),
|
||||
"azure",
|
||||
);
|
||||
});
|
||||
|
||||
test("detectProvider infers Seedream from model id and allows Seedream reference-image workflows", (t) => {
|
||||
useEnv(t, {
|
||||
GOOGLE_API_KEY: null,
|
||||
|
||||
@@ -60,6 +60,7 @@ const DEFAULT_PROVIDER_RATE_LIMITS: Record<Provider, ProviderRateLimit> = {
|
||||
dashscope: { concurrency: 3, startIntervalMs: 1100 },
|
||||
jimeng: { concurrency: 3, startIntervalMs: 1100 },
|
||||
seedream: { concurrency: 3, startIntervalMs: 1100 },
|
||||
azure: { concurrency: 3, startIntervalMs: 1100 },
|
||||
};
|
||||
|
||||
function printUsage(): void {
|
||||
@@ -74,13 +75,13 @@ Options:
|
||||
--image <path> Output image path (required in single-image mode)
|
||||
--batchfile <path> JSON batch file for multi-image generation
|
||||
--jobs <count> Worker count for batch mode (default: auto, max from config, built-in default 10)
|
||||
--provider google|openai|openrouter|dashscope|replicate|jimeng|seedream Force provider (auto-detect by default)
|
||||
--provider google|openai|openrouter|dashscope|replicate|jimeng|seedream|azure Force provider (auto-detect by default)
|
||||
-m, --model <id> Model ID
|
||||
--ar <ratio> Aspect ratio (e.g., 16:9, 1:1, 4:3)
|
||||
--size <WxH> Size (e.g., 1024x1024)
|
||||
--quality normal|2k Quality preset (default: 2k)
|
||||
--imageSize 1K|2K|4K Image size for Google/OpenRouter (default: from quality)
|
||||
--ref <files...> Reference images (Google, OpenAI, OpenRouter, Replicate, or Seedream 4.0/4.5/5.0)
|
||||
--ref <files...> Reference images (Google, OpenAI, Azure, OpenRouter, Replicate, or Seedream 4.0/4.5/5.0)
|
||||
--n <count> Number of images for the current task (default: 1)
|
||||
--json JSON output
|
||||
-h, --help Show help
|
||||
@@ -131,6 +132,11 @@ Environment variables:
|
||||
DASHSCOPE_BASE_URL Custom DashScope endpoint
|
||||
REPLICATE_BASE_URL Custom Replicate endpoint
|
||||
JIMENG_BASE_URL Custom Jimeng endpoint
|
||||
AZURE_OPENAI_API_KEY Azure OpenAI API key
|
||||
AZURE_OPENAI_BASE_URL Azure OpenAI resource or deployment endpoint
|
||||
AZURE_OPENAI_DEPLOYMENT Default Azure deployment name
|
||||
AZURE_API_VERSION Azure API version (default: 2025-04-01-preview)
|
||||
AZURE_OPENAI_IMAGE_MODEL Backward-compatible Azure deployment/model alias (defaults to gpt-image-1.5)
|
||||
SEEDREAM_BASE_URL Custom Seedream endpoint
|
||||
BAOYU_IMAGE_GEN_MAX_WORKERS Override batch worker cap
|
||||
BAOYU_IMAGE_GEN_<PROVIDER>_CONCURRENCY Override provider concurrency
|
||||
@@ -231,7 +237,8 @@ export function parseArgs(argv: string[]): CliArgs {
|
||||
v !== "dashscope" &&
|
||||
v !== "replicate" &&
|
||||
v !== "jimeng" &&
|
||||
v !== "seedream"
|
||||
v !== "seedream" &&
|
||||
v !== "azure"
|
||||
) {
|
||||
throw new Error(`Invalid provider: ${v}`);
|
||||
}
|
||||
@@ -386,6 +393,7 @@ export function parseSimpleYaml(yaml: string): Partial<ExtendConfig> {
|
||||
replicate: null,
|
||||
jimeng: null,
|
||||
seedream: null,
|
||||
azure: null,
|
||||
};
|
||||
currentKey = "default_model";
|
||||
currentProvider = null;
|
||||
@@ -411,7 +419,8 @@ export function parseSimpleYaml(yaml: string): Partial<ExtendConfig> {
|
||||
key === "dashscope" ||
|
||||
key === "replicate" ||
|
||||
key === "jimeng" ||
|
||||
key === "seedream"
|
||||
key === "seedream" ||
|
||||
key === "azure"
|
||||
)
|
||||
) {
|
||||
config.batch ??= {};
|
||||
@@ -427,7 +436,8 @@ export function parseSimpleYaml(yaml: string): Partial<ExtendConfig> {
|
||||
key === "dashscope" ||
|
||||
key === "replicate" ||
|
||||
key === "jimeng" ||
|
||||
key === "seedream"
|
||||
key === "seedream" ||
|
||||
key === "azure"
|
||||
)
|
||||
) {
|
||||
const cleaned = value.replace(/['"]/g, "");
|
||||
@@ -520,9 +530,10 @@ export function getConfiguredProviderRateLimits(
|
||||
dashscope: { ...DEFAULT_PROVIDER_RATE_LIMITS.dashscope },
|
||||
jimeng: { ...DEFAULT_PROVIDER_RATE_LIMITS.jimeng },
|
||||
seedream: { ...DEFAULT_PROVIDER_RATE_LIMITS.seedream },
|
||||
azure: { ...DEFAULT_PROVIDER_RATE_LIMITS.azure },
|
||||
};
|
||||
|
||||
for (const provider of ["replicate", "google", "openai", "openrouter", "dashscope", "jimeng", "seedream"] as Provider[]) {
|
||||
for (const provider of ["replicate", "google", "openai", "openrouter", "dashscope", "jimeng", "seedream", "azure"] as Provider[]) {
|
||||
const envPrefix = `BAOYU_IMAGE_GEN_${provider.toUpperCase()}`;
|
||||
const extendLimit = extendConfig.batch?.provider_limits?.[provider];
|
||||
configured[provider] = {
|
||||
@@ -581,18 +592,20 @@ export function detectProvider(args: CliArgs): Provider {
|
||||
args.provider &&
|
||||
args.provider !== "google" &&
|
||||
args.provider !== "openai" &&
|
||||
args.provider !== "azure" &&
|
||||
args.provider !== "openrouter" &&
|
||||
args.provider !== "replicate" &&
|
||||
args.provider !== "seedream"
|
||||
) {
|
||||
throw new Error(
|
||||
"Reference images require a ref-capable provider. Use --provider google (Gemini multimodal), --provider openai (GPT Image edits), --provider openrouter (OpenRouter multimodal), --provider replicate, or --provider seedream for supported Seedream models."
|
||||
"Reference images require a ref-capable provider. Use --provider google (Gemini multimodal), --provider openai (GPT Image edits), --provider azure (Azure OpenAI), --provider openrouter (OpenRouter multimodal), --provider replicate, or --provider seedream for supported Seedream models."
|
||||
);
|
||||
}
|
||||
|
||||
if (args.provider) return args.provider;
|
||||
|
||||
const hasGoogle = !!(process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY);
|
||||
const hasAzure = !!(process.env.AZURE_OPENAI_API_KEY && process.env.AZURE_OPENAI_BASE_URL);
|
||||
const hasOpenai = !!process.env.OPENAI_API_KEY;
|
||||
const hasOpenrouter = !!process.env.OPENROUTER_API_KEY;
|
||||
const hasDashscope = !!process.env.DASHSCOPE_API_KEY;
|
||||
@@ -611,17 +624,19 @@ export function detectProvider(args: CliArgs): Provider {
|
||||
if (args.referenceImages.length > 0) {
|
||||
if (hasGoogle) return "google";
|
||||
if (hasOpenai) return "openai";
|
||||
if (hasAzure) return "azure";
|
||||
if (hasOpenrouter) return "openrouter";
|
||||
if (hasReplicate) return "replicate";
|
||||
if (hasSeedream) return "seedream";
|
||||
throw new Error(
|
||||
"Reference images require Google, OpenAI, OpenRouter, Replicate, or supported Seedream models. Set GOOGLE_API_KEY/GEMINI_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, REPLICATE_API_TOKEN, or ARK_API_KEY, or remove --ref."
|
||||
"Reference images require Google, OpenAI, Azure, OpenRouter, Replicate, or supported Seedream models. Set GOOGLE_API_KEY/GEMINI_API_KEY, OPENAI_API_KEY, AZURE_OPENAI_API_KEY+AZURE_OPENAI_BASE_URL, OPENROUTER_API_KEY, REPLICATE_API_TOKEN, or ARK_API_KEY, or remove --ref."
|
||||
);
|
||||
}
|
||||
|
||||
const available = [
|
||||
hasGoogle && "google",
|
||||
hasOpenai && "openai",
|
||||
hasAzure && "azure",
|
||||
hasOpenrouter && "openrouter",
|
||||
hasDashscope && "dashscope",
|
||||
hasReplicate && "replicate",
|
||||
@@ -633,7 +648,7 @@ export function detectProvider(args: CliArgs): Provider {
|
||||
if (available.length > 1) return available[0]!;
|
||||
|
||||
throw new Error(
|
||||
"No API key found. Set GOOGLE_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, DASHSCOPE_API_KEY, REPLICATE_API_TOKEN, JIMENG keys, or ARK_API_KEY.\n" +
|
||||
"No API key found. Set GOOGLE_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY, AZURE_OPENAI_API_KEY+AZURE_OPENAI_BASE_URL, OPENROUTER_API_KEY, DASHSCOPE_API_KEY, REPLICATE_API_TOKEN, JIMENG keys, or ARK_API_KEY.\n" +
|
||||
"Create ~/.baoyu-skills/.env or <cwd>/.baoyu-skills/.env with your keys."
|
||||
);
|
||||
}
|
||||
@@ -676,6 +691,7 @@ async function loadProviderModule(provider: Provider): Promise<ProviderModule> {
|
||||
if (provider === "openrouter") return (await import("./providers/openrouter")) as ProviderModule;
|
||||
if (provider === "jimeng") return (await import("./providers/jimeng")) as ProviderModule;
|
||||
if (provider === "seedream") return (await import("./providers/seedream")) as ProviderModule;
|
||||
if (provider === "azure") return (await import("./providers/azure")) as ProviderModule;
|
||||
return (await import("./providers/openai")) as ProviderModule;
|
||||
}
|
||||
|
||||
@@ -704,6 +720,7 @@ function getModelForProvider(
|
||||
if (provider === "replicate" && extendConfig.default_model.replicate) return extendConfig.default_model.replicate;
|
||||
if (provider === "jimeng" && extendConfig.default_model.jimeng) return extendConfig.default_model.jimeng;
|
||||
if (provider === "seedream" && extendConfig.default_model.seedream) return extendConfig.default_model.seedream;
|
||||
if (provider === "azure" && extendConfig.default_model.azure) return extendConfig.default_model.azure;
|
||||
}
|
||||
return providerModule.getDefaultModel();
|
||||
}
|
||||
@@ -923,7 +940,7 @@ async function runBatchTasks(
|
||||
const acquireProvider = createProviderGate(providerRateLimits);
|
||||
const workerCount = getWorkerCount(tasks.length, jobs, maxWorkers);
|
||||
console.error(`Batch mode: ${tasks.length} tasks, ${workerCount} workers, parallel mode enabled.`);
|
||||
for (const provider of ["replicate", "google", "openai", "openrouter", "dashscope", "jimeng", "seedream"] as Provider[]) {
|
||||
for (const provider of ["replicate", "google", "openai", "openrouter", "dashscope", "jimeng", "seedream", "azure"] as Provider[]) {
|
||||
const limit = providerRateLimits[provider];
|
||||
console.error(`- ${provider}: concurrency=${limit.concurrency}, startIntervalMs=${limit.startIntervalMs}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test, { type TestContext } from "node:test";
|
||||
|
||||
import type { CliArgs } from "../types.ts";
|
||||
import {
|
||||
generateImage,
|
||||
getDefaultModel,
|
||||
parseAzureBaseURL,
|
||||
validateArgs,
|
||||
} from "./azure.ts";
|
||||
|
||||
function useEnv(
|
||||
t: TestContext,
|
||||
values: Record<string, string | null>,
|
||||
): void {
|
||||
const previous = new Map<string, string | undefined>();
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
previous.set(key, process.env[key]);
|
||||
if (value == null) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
t.after(() => {
|
||||
for (const [key, value] of previous.entries()) {
|
||||
if (value == null) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
return {
|
||||
prompt: null,
|
||||
promptFiles: [],
|
||||
imagePath: null,
|
||||
provider: null,
|
||||
model: null,
|
||||
aspectRatio: null,
|
||||
size: null,
|
||||
quality: null,
|
||||
imageSize: null,
|
||||
referenceImages: [],
|
||||
n: 1,
|
||||
batchFile: null,
|
||||
jobs: null,
|
||||
json: false,
|
||||
help: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function makeTempDir(prefix: string): Promise<string> {
|
||||
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||
}
|
||||
|
||||
test("Azure endpoint parsing and default deployment selection follow env precedence", (t) => {
|
||||
assert.deepEqual(parseAzureBaseURL("https://example.openai.azure.com"), {
|
||||
resourceBaseURL: "https://example.openai.azure.com/openai",
|
||||
deployment: null,
|
||||
});
|
||||
assert.deepEqual(
|
||||
parseAzureBaseURL("https://example.openai.azure.com/openai/deployments/from-url"),
|
||||
{
|
||||
resourceBaseURL: "https://example.openai.azure.com/openai",
|
||||
deployment: "from-url",
|
||||
},
|
||||
);
|
||||
|
||||
useEnv(t, {
|
||||
AZURE_OPENAI_BASE_URL: "https://example.openai.azure.com/openai/deployments/from-url",
|
||||
AZURE_OPENAI_DEPLOYMENT: "explicit-deploy",
|
||||
AZURE_OPENAI_IMAGE_MODEL: "env-fallback",
|
||||
});
|
||||
assert.equal(getDefaultModel(), "explicit-deploy");
|
||||
});
|
||||
|
||||
test("Azure validateArgs rejects unsupported edit input formats before the API call", () => {
|
||||
assert.doesNotThrow(() =>
|
||||
validateArgs("demo-deployment", makeArgs({ referenceImages: ["hero.png", "photo.jpeg"] })),
|
||||
);
|
||||
assert.throws(
|
||||
() => validateArgs("demo-deployment", makeArgs({ referenceImages: ["hero.webp"] })),
|
||||
/PNG or JPG\/JPEG/,
|
||||
);
|
||||
});
|
||||
|
||||
test("Azure image generation routes model to deployment and sends mapped quality", async (t) => {
|
||||
useEnv(t, {
|
||||
AZURE_OPENAI_API_KEY: "azure-key",
|
||||
AZURE_OPENAI_BASE_URL: "https://example.openai.azure.com/openai/deployments/default-deploy",
|
||||
AZURE_API_VERSION: null,
|
||||
AZURE_OPENAI_DEPLOYMENT: null,
|
||||
AZURE_OPENAI_IMAGE_MODEL: null,
|
||||
});
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
t.after(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
const calls: Array<{ url: string; body: string }> = [];
|
||||
globalThis.fetch = async (input, init) => {
|
||||
calls.push({
|
||||
url: String(input),
|
||||
body: String(init?.body ?? ""),
|
||||
});
|
||||
return Response.json({
|
||||
data: [{ b64_json: Buffer.from("azure-image").toString("base64") }],
|
||||
});
|
||||
};
|
||||
|
||||
const bytes = await generateImage(
|
||||
"A calm lake at sunset",
|
||||
"custom-deploy",
|
||||
makeArgs({ quality: "normal" }),
|
||||
);
|
||||
|
||||
assert.equal(Buffer.from(bytes).toString("utf8"), "azure-image");
|
||||
assert.equal(
|
||||
calls[0]?.url,
|
||||
"https://example.openai.azure.com/openai/deployments/custom-deploy/images/generations?api-version=2025-04-01-preview",
|
||||
);
|
||||
|
||||
const body = JSON.parse(calls[0]!.body) as Record<string, string>;
|
||||
assert.equal(body.quality, "medium");
|
||||
assert.equal(body.size, "1024x1024");
|
||||
});
|
||||
|
||||
test("Azure image edits include quality in multipart requests", async (t) => {
|
||||
const root = await makeTempDir("baoyu-image-gen-azure-");
|
||||
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||
|
||||
const pngPath = path.join(root, "ref.png");
|
||||
const jpgPath = path.join(root, "ref.jpg");
|
||||
await fs.writeFile(pngPath, "png-bytes");
|
||||
await fs.writeFile(jpgPath, "jpg-bytes");
|
||||
|
||||
useEnv(t, {
|
||||
AZURE_OPENAI_API_KEY: "azure-key",
|
||||
AZURE_OPENAI_BASE_URL: "https://example.openai.azure.com",
|
||||
AZURE_API_VERSION: "2025-04-01-preview",
|
||||
AZURE_OPENAI_DEPLOYMENT: null,
|
||||
AZURE_OPENAI_IMAGE_MODEL: null,
|
||||
});
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
t.after(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
const calls: Array<{ url: string; form: FormData }> = [];
|
||||
globalThis.fetch = async (input, init) => {
|
||||
calls.push({
|
||||
url: String(input),
|
||||
form: init?.body as FormData,
|
||||
});
|
||||
return Response.json({
|
||||
data: [{ b64_json: Buffer.from("edited-image").toString("base64") }],
|
||||
});
|
||||
};
|
||||
|
||||
const bytes = await generateImage(
|
||||
"Add warm lighting",
|
||||
"edit-deploy",
|
||||
makeArgs({
|
||||
quality: "2k",
|
||||
referenceImages: [pngPath, jpgPath],
|
||||
}),
|
||||
);
|
||||
|
||||
assert.equal(Buffer.from(bytes).toString("utf8"), "edited-image");
|
||||
assert.equal(
|
||||
calls[0]?.url,
|
||||
"https://example.openai.azure.com/openai/deployments/edit-deploy/images/edits?api-version=2025-04-01-preview",
|
||||
);
|
||||
assert.equal(calls[0]?.form.get("quality"), "high");
|
||||
assert.equal(calls[0]?.form.get("size"), "1024x1024");
|
||||
assert.equal(calls[0]?.form.getAll("image[]").length, 2);
|
||||
});
|
||||
@@ -0,0 +1,192 @@
|
||||
import path from "node:path";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import type { CliArgs } from "../types";
|
||||
import { getOpenAISize, extractImageFromResponse } from "./openai.ts";
|
||||
|
||||
type OpenAIImageResponse = { data: Array<{ url?: string; b64_json?: string }> };
|
||||
type AzureEndpoint = {
|
||||
resourceBaseURL: string;
|
||||
deployment: string | null;
|
||||
};
|
||||
|
||||
const DEFAULT_AZURE_API_VERSION = "2025-04-01-preview";
|
||||
const AZURE_EDIT_IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg"]);
|
||||
|
||||
export function parseAzureBaseURL(url: string): AzureEndpoint {
|
||||
const parsed = new URL(url);
|
||||
const trimmedPath = parsed.pathname.replace(/\/+$/, "");
|
||||
const deploymentMatch = trimmedPath.match(/^(.*?)(?:\/openai)?\/deployments\/([^/]+)$/);
|
||||
|
||||
if (deploymentMatch) {
|
||||
parsed.pathname = `${deploymentMatch[1] || ""}/openai`;
|
||||
return {
|
||||
resourceBaseURL: parsed.toString().replace(/\/+$/, ""),
|
||||
deployment: decodeURIComponent(deploymentMatch[2]!),
|
||||
};
|
||||
}
|
||||
|
||||
parsed.pathname = trimmedPath.endsWith("/openai") ? trimmedPath : `${trimmedPath}/openai`;
|
||||
return {
|
||||
resourceBaseURL: parsed.toString().replace(/\/+$/, ""),
|
||||
deployment: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function getDefaultModel(): string {
|
||||
const explicitDeployment = process.env.AZURE_OPENAI_DEPLOYMENT?.trim();
|
||||
if (explicitDeployment) return explicitDeployment;
|
||||
|
||||
const baseURL = process.env.AZURE_OPENAI_BASE_URL;
|
||||
if (baseURL) {
|
||||
try {
|
||||
const { deployment } = parseAzureBaseURL(baseURL);
|
||||
if (deployment) return deployment;
|
||||
} catch {
|
||||
// Ignore invalid URLs here so the required-env check can raise the user-facing error later.
|
||||
}
|
||||
}
|
||||
|
||||
return process.env.AZURE_OPENAI_IMAGE_MODEL || "gpt-image-1.5";
|
||||
}
|
||||
|
||||
function getEndpoint(): AzureEndpoint {
|
||||
const url = process.env.AZURE_OPENAI_BASE_URL;
|
||||
if (!url) {
|
||||
throw new Error(
|
||||
"AZURE_OPENAI_BASE_URL is required. Set it to your Azure resource or deployment endpoint, e.g.: https://your-resource.openai.azure.com or https://your-resource.openai.azure.com/openai/deployments/your-deployment"
|
||||
);
|
||||
}
|
||||
return parseAzureBaseURL(url);
|
||||
}
|
||||
|
||||
function getApiKey(): string {
|
||||
const key = process.env.AZURE_OPENAI_API_KEY;
|
||||
if (!key) {
|
||||
throw new Error(
|
||||
"AZURE_OPENAI_API_KEY is required. Get it from Azure Portal → your OpenAI resource → Keys and Endpoint."
|
||||
);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
function getApiVersion(): string {
|
||||
return process.env.AZURE_API_VERSION || DEFAULT_AZURE_API_VERSION;
|
||||
}
|
||||
|
||||
function getDeployment(model: string): string {
|
||||
const deployment = model.trim();
|
||||
if (!deployment) {
|
||||
throw new Error(
|
||||
"Azure deployment name is required. Use --model <deployment>, AZURE_OPENAI_DEPLOYMENT, AZURE_OPENAI_IMAGE_MODEL, or embed the deployment in AZURE_OPENAI_BASE_URL."
|
||||
);
|
||||
}
|
||||
return deployment;
|
||||
}
|
||||
|
||||
function buildURL(deployment: string, pathSuffix: string): string {
|
||||
const { resourceBaseURL } = getEndpoint();
|
||||
return `${resourceBaseURL}/deployments/${encodeURIComponent(deployment)}${pathSuffix}?api-version=${getApiVersion()}`;
|
||||
}
|
||||
|
||||
function authHeaders(): Record<string, string> {
|
||||
return { "api-key": getApiKey() };
|
||||
}
|
||||
|
||||
function getAzureQuality(quality: CliArgs["quality"]): "medium" | "high" {
|
||||
return quality === "2k" ? "high" : "medium";
|
||||
}
|
||||
|
||||
export function validateArgs(_model: string, args: CliArgs): void {
|
||||
for (const refPath of args.referenceImages) {
|
||||
const ext = path.extname(refPath).toLowerCase();
|
||||
if (!AZURE_EDIT_IMAGE_EXTENSIONS.has(ext)) {
|
||||
throw new Error(
|
||||
`Azure OpenAI reference images must be PNG or JPG/JPEG. Unsupported file: ${refPath}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateImage(
|
||||
prompt: string,
|
||||
model: string,
|
||||
args: CliArgs
|
||||
): Promise<Uint8Array> {
|
||||
const deployment = getDeployment(model);
|
||||
const size = args.size || getOpenAISize(model, args.aspectRatio, args.quality);
|
||||
|
||||
if (args.referenceImages.length > 0) {
|
||||
return generateWithAzureEdits(prompt, deployment, size, args.referenceImages, args.quality);
|
||||
}
|
||||
|
||||
return generateWithAzureGenerations(prompt, deployment, size, args.quality);
|
||||
}
|
||||
|
||||
async function generateWithAzureGenerations(
|
||||
prompt: string,
|
||||
deployment: string,
|
||||
size: string,
|
||||
quality: CliArgs["quality"]
|
||||
): Promise<Uint8Array> {
|
||||
const body: Record<string, any> = {
|
||||
prompt,
|
||||
size,
|
||||
n: 1,
|
||||
quality: getAzureQuality(quality),
|
||||
};
|
||||
|
||||
const res = await fetch(buildURL(deployment, "/images/generations"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...authHeaders(),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`Azure OpenAI API error: ${err}`);
|
||||
}
|
||||
|
||||
const result = (await res.json()) as OpenAIImageResponse;
|
||||
return extractImageFromResponse(result);
|
||||
}
|
||||
|
||||
async function generateWithAzureEdits(
|
||||
prompt: string,
|
||||
deployment: string,
|
||||
size: string,
|
||||
referenceImages: string[],
|
||||
quality: CliArgs["quality"]
|
||||
): Promise<Uint8Array> {
|
||||
const form = new FormData();
|
||||
form.append("prompt", prompt);
|
||||
form.append("size", size);
|
||||
form.append("n", "1");
|
||||
form.append("quality", getAzureQuality(quality));
|
||||
|
||||
for (const refPath of referenceImages) {
|
||||
const bytes = await readFile(refPath);
|
||||
const filename = path.basename(refPath);
|
||||
const mimeType = path.extname(filename).toLowerCase() === ".png" ? "image/png" : "image/jpeg";
|
||||
const blob = new Blob([bytes], { type: mimeType });
|
||||
form.append("image[]", blob, filename);
|
||||
}
|
||||
|
||||
const res = await fetch(buildURL(deployment, "/images/edits"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...authHeaders(),
|
||||
},
|
||||
body: form,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`Azure OpenAI edits API error: ${err}`);
|
||||
}
|
||||
|
||||
const result = (await res.json()) as OpenAIImageResponse;
|
||||
return extractImageFromResponse(result);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export type Provider = "google" | "openai" | "openrouter" | "dashscope" | "replicate" | "jimeng" | "seedream";
|
||||
export type Provider = "google" | "openai" | "openrouter" | "dashscope" | "replicate" | "jimeng" | "seedream" | "azure";
|
||||
export type Quality = "normal" | "2k";
|
||||
|
||||
export type CliArgs = {
|
||||
@@ -55,6 +55,7 @@ export type ExtendConfig = {
|
||||
replicate: string | null;
|
||||
jimeng: string | null;
|
||||
seedream: string | null;
|
||||
azure: string | null;
|
||||
};
|
||||
batch?: {
|
||||
max_workers?: number | null;
|
||||
|
||||
Reference in New Issue
Block a user