@gobing-ai/knowledge-kit 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +20 -0
  3. package/package.json +1 -1
  4. package/plugins/generations/content-gen/dist/index.js +20 -0
  5. package/plugins/generations/core-facts-gen/dist/index.js +20 -0
  6. package/plugins/generations/daily-article-gen/dist/index.js +29 -1
  7. package/plugins/generations/daily-article-gen/src/index.ts +13 -1
  8. package/plugins/generations/dailynews-gen/dist/index.js +20 -0
  9. package/plugins/generations/episode-plan-gen/dist/index.js +24 -0
  10. package/plugins/generations/episode-plan-gen/src/index.ts +11 -0
  11. package/plugins/generations/image-gen/dist/index.js +2296 -53
  12. package/plugins/generations/image-gen/src/providers/agnes.ts +110 -0
  13. package/plugins/generations/image-gen/src/providers/azure.ts +153 -0
  14. package/plugins/generations/image-gen/src/providers/codex-cli.ts +170 -0
  15. package/plugins/generations/image-gen/src/providers/dashscope.ts +485 -0
  16. package/plugins/generations/image-gen/src/providers/google.ts +268 -0
  17. package/plugins/generations/image-gen/src/providers/huggingface.ts +59 -0
  18. package/plugins/generations/image-gen/src/providers/jimeng.ts +259 -0
  19. package/plugins/generations/image-gen/src/providers/minimax.ts +171 -0
  20. package/plugins/generations/image-gen/src/providers/openai.ts +319 -0
  21. package/plugins/generations/image-gen/src/providers/openrouter.ts +257 -0
  22. package/plugins/generations/image-gen/src/providers/refs.ts +24 -0
  23. package/plugins/generations/image-gen/src/providers/replicate.ts +279 -0
  24. package/plugins/generations/image-gen/src/providers/seedream.ts +1 -1
  25. package/plugins/generations/image-gen/src/providers/types.ts +52 -51
  26. package/plugins/generations/image-gen/src/providers/zai.ts +237 -0
  27. package/plugins/generations/news-report-gen/dist/index.js +22193 -0
  28. package/plugins/generations/news-report-gen/package.json +17 -0
  29. package/plugins/generations/news-report-gen/plugin.json +7 -0
  30. package/plugins/generations/news-report-gen/src/index.ts +308 -0
  31. package/plugins/generations/news-report-gen/tsconfig.json +4 -0
  32. package/plugins/generations/omni-voice-gen/Makefile +14 -0
  33. package/plugins/generations/omni-voice-gen/README.md +112 -0
  34. package/plugins/generations/omni-voice-gen/bin/omni-voice-gen +2 -0
  35. package/plugins/generations/omni-voice-gen/dist/omni-voice-gen-prr8skpb. +2 -0
  36. package/plugins/generations/omni-voice-gen/dist/omni-voice-gen.js +6 -0
  37. package/plugins/generations/omni-voice-gen/plugin.json +6 -0
  38. package/plugins/generations/omni-voice-gen/profiles.json +12 -0
  39. package/plugins/generations/omni-voice-gen/pyproject.toml +25 -0
  40. package/plugins/generations/omni-voice-gen/scripts/coverage_gate.py +74 -0
  41. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__init__.py +1 -0
  42. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__main__.py +39 -0
  43. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/audio.py +190 -0
  44. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/backend.py +150 -0
  45. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/contract.py +76 -0
  46. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/mp3.py +60 -0
  47. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +289 -0
  48. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/profiles.py +100 -0
  49. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/qc.py +234 -0
  50. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/voicescript.py +352 -0
  51. package/plugins/generations/omni-voice-gen/uv.lock +3510 -0
  52. package/plugins/generations/voice-gen/dist/index.js +30 -2
  53. package/plugins/generations/voice-gen/src/index.ts +16 -1
  54. package/plugins/generations/voice-gen/src/voicebox-client.ts +3 -1
  55. package/plugins/ingestions/aihot-ingest/dist/index.js +20 -0
  56. package/plugins/ingestions/horizon-ingest/dist/index.js +20 -0
  57. package/plugins/ingestions/last30days-ingest/dist/index.js +20 -0
  58. package/plugins/ingestions/web-search/dist/index.js +20 -0
  59. package/plugins/kk/commands/image-extract.md +40 -0
  60. package/plugins/kk/commands/image-generate.md +18 -1
  61. package/plugins/kk/plugin.json +1 -1
  62. package/plugins/kk/skills/image-authoring/SKILL.md +10 -3
  63. package/plugins/kk/skills/image-authoring/references/format-drafting.md +57 -0
  64. package/plugins/kk/skills/image-authoring/references/style-extraction.md +14 -9
  65. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +130 -30
  66. package/plugins/publishings/emdash-pub/dist/index.js +20 -0
  67. package/plugins/publishings/podcast-pub/dist/index.js +57 -3
  68. package/plugins/publishings/podcast-pub/src/index.ts +18 -2
  69. package/plugins/publishings/podcast-pub/src/show-notes.ts +56 -9
  70. package/plugins/publishings/qiita-pub/dist/index.js +20 -0
  71. package/plugins/publishings/surfdash-pub/dist/index.js +96 -6
  72. package/plugins/publishings/surfdash-pub/src/index.ts +109 -9
  73. package/plugins/publishings/zenn-pub/dist/index.js +20 -0
  74. package/plugins/sp/scripts/batch-preflight.mjs +346 -0
  75. package/plugins/sp/scripts/batch-preflight.ts +459 -0
  76. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +615 -0
  77. package/plugins/sp/scripts/daily-summary/daily-summary.ts +846 -0
  78. package/plugins/sp/scripts/daily-summary/logger.ts +28 -0
  79. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +223 -0
  80. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +367 -0
  81. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +132 -0
  82. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +169 -0
  83. package/plugins/sp/scripts/feature-dev-precheck.mjs +171 -0
  84. package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
  85. package/plugins/sp/scripts/feature-sync-bounded.mjs +285 -0
  86. package/plugins/sp/scripts/feature-sync-bounded.ts +478 -0
  87. package/plugins/sp/scripts/history-anatomy-cache.mjs +902 -0
  88. package/plugins/sp/scripts/history-anatomy-cache.ts +1028 -0
  89. package/plugins/sp/scripts/idea-handoff.mjs +22 -0
  90. package/plugins/sp/scripts/idea-handoff.ts +44 -0
  91. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +185 -0
  92. package/plugins/sp/scripts/inline-run-setup.ts +198 -0
  93. package/plugins/sp/scripts/pr-reviewing.mjs +769 -0
  94. package/plugins/sp/scripts/pr-reviewing.ts +925 -0
  95. package/plugins/sp/scripts/quality-gate.mjs +179 -0
  96. package/plugins/sp/scripts/quality-gate.ts +217 -0
  97. package/plugins/sp/scripts/script-contract-check.ts +319 -0
  98. package/plugins/sp/scripts/stage-registry-adapter.ts +1533 -0
  99. package/plugins/sp/scripts/surface-drift-inventory.ts +929 -0
  100. package/plugins/sp/scripts/task-evidence-precheck.ts +181 -0
  101. package/plugins/sp/scripts/task-size-precheck.ts +175 -0
  102. package/plugins/sp/scripts/transition-shim-check.ts +238 -0
  103. package/plugins/sp/scripts/validate-commands.ts +689 -0
  104. package/plugins/sp/scripts/validate-flag-contracts.ts +878 -0
  105. package/plugins/sp/scripts/verify-answer-lint.ts +530 -0
  106. package/plugins/sp/scripts/workflow-step-profile.mjs +316 -0
  107. package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
  108. package/plugins/sp/scripts/wrapup-steps.mjs +373 -0
  109. package/plugins/sp/scripts/wrapup-steps.ts +466 -0
@@ -0,0 +1,171 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { readBytes } from '../bytes';
3
+ import { mimeForPath } from './refs';
4
+ import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
5
+
6
+ /**
7
+ * MiniMax adapter (0115 R1) — port of
8
+ * vendors/baoyu-skills/skills/baoyu-image-gen/scripts/providers/minimax.ts
9
+ * (MIT © 2026 Jim Liu). `CliArgs.size` becomes `ImageRequest.width/height`.
10
+ */
11
+
12
+ const DEFAULT_MODEL = 'image-01';
13
+ const MAX_REFERENCE_IMAGE_BYTES = 10 * 1024 * 1024;
14
+ const SUPPORTED_ASPECT_RATIOS = new Set(['1:1', '16:9', '4:3', '3:2', '2:3', '3:4', '9:16', '21:9']);
15
+
16
+ interface MinimaxSubjectReference {
17
+ type: 'character';
18
+ image_file: string;
19
+ }
20
+
21
+ interface MinimaxRequestBody {
22
+ model: string;
23
+ prompt: string;
24
+ response_format: 'base64';
25
+ aspect_ratio?: string;
26
+ width?: number;
27
+ height?: number;
28
+ n?: number;
29
+ subject_reference?: MinimaxSubjectReference[];
30
+ }
31
+
32
+ interface MinimaxResponse {
33
+ data?: {
34
+ image_urls?: string[];
35
+ image_base64?: string[];
36
+ };
37
+ base_resp?: {
38
+ status_code?: number;
39
+ status_msg?: string;
40
+ };
41
+ }
42
+
43
+ export function parsePixelSize(size: string): { width: number; height: number } | null {
44
+ const match = size.trim().match(/^(\d+)\s*[xX*]\s*(\d+)$/);
45
+ if (match === null) return null;
46
+ const width = Number.parseInt(match[1] as string, 10);
47
+ const height = Number.parseInt(match[2] as string, 10);
48
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null;
49
+ return { width, height };
50
+ }
51
+
52
+ function validatePixelSize(width: number, height: number): void {
53
+ if (width < 512 || width > 2048 || height < 512 || height > 2048) {
54
+ throw new Error('MiniMax custom size must keep width and height between 512 and 2048.');
55
+ }
56
+ if (width % 8 !== 0 || height % 8 !== 0) {
57
+ throw new Error('MiniMax custom size requires width and height divisible by 8.');
58
+ }
59
+ }
60
+
61
+ export function requestBody(
62
+ prompt: string,
63
+ model: string,
64
+ req: Pick<ImageRequest, 'aspectRatio' | 'width' | 'height' | 'n' | 'referenceImages'>,
65
+ ): MinimaxRequestBody {
66
+ if ((req.n ?? 1) > 9) throw new Error('MiniMax supports at most 9 images per request.');
67
+ if (req.aspectRatio !== undefined && !SUPPORTED_ASPECT_RATIOS.has(req.aspectRatio)) {
68
+ throw new Error(`MiniMax aspect_ratio must be one of: ${Array.from(SUPPORTED_ASPECT_RATIOS).join(', ')}.`);
69
+ }
70
+ if (req.width !== undefined && req.height !== undefined && req.aspectRatio === undefined) {
71
+ if (model !== 'image-01') {
72
+ throw new Error(
73
+ 'MiniMax custom --size is only supported with model image-01. Use --model image-01 or pass --ar instead.',
74
+ );
75
+ }
76
+ validatePixelSize(req.width, req.height);
77
+ }
78
+ if (req.referenceImages !== undefined && req.referenceImages.length > 0) {
79
+ for (const refPath of req.referenceImages) {
80
+ const ext = refPath.toLowerCase();
81
+ if (!ext.includes('.jpg') && !ext.includes('.jpeg') && !ext.includes('.png')) {
82
+ throw new Error(`MiniMax subject_reference only supports JPG, JPEG, or PNG files: ${refPath}`);
83
+ }
84
+ }
85
+ }
86
+ const body: MinimaxRequestBody = { model, prompt, response_format: 'base64' };
87
+ if (req.aspectRatio !== undefined) {
88
+ body.aspect_ratio = req.aspectRatio;
89
+ } else if (req.width !== undefined && req.height !== undefined) {
90
+ body.width = req.width;
91
+ body.height = req.height;
92
+ }
93
+ if ((req.n ?? 1) > 1) body.n = req.n;
94
+ return body;
95
+ }
96
+
97
+ export async function buildSubjectReference(referenceImages: string[]): Promise<MinimaxSubjectReference[] | undefined> {
98
+ if (referenceImages.length === 0) return undefined;
99
+ const subjectReference: MinimaxSubjectReference[] = [];
100
+ for (const refPath of referenceImages) {
101
+ const ext = refPath.toLowerCase();
102
+ if (!ext.includes('.jpg') && !ext.includes('.jpeg') && !ext.includes('.png')) {
103
+ throw new Error(`MiniMax subject_reference only supports JPG, JPEG, or PNG files: ${refPath}`);
104
+ }
105
+ const bytes = Buffer.from(await readBytes(refPath));
106
+ if (bytes.length > MAX_REFERENCE_IMAGE_BYTES) {
107
+ throw new Error(`MiniMax subject_reference images must be smaller than 10MB: ${refPath}`);
108
+ }
109
+ subjectReference.push({
110
+ type: 'character',
111
+ image_file: `data:${mimeForPath(refPath)};base64,${bytes.toString('base64')}`,
112
+ });
113
+ }
114
+ return subjectReference;
115
+ }
116
+
117
+ export async function extractImageFromResponse(
118
+ result: MinimaxResponse,
119
+ fetchFn: typeof fetch = fetch,
120
+ ): Promise<Uint8Array> {
121
+ const baseResp = result.base_resp;
122
+ if (baseResp?.status_code !== undefined && baseResp.status_code !== 0) {
123
+ throw new Error(baseResp.status_msg ?? `MiniMax API returned status_code=${baseResp.status_code}`);
124
+ }
125
+ const base64Image = result.data?.image_base64?.[0];
126
+ if (base64Image !== undefined) return Uint8Array.from(Buffer.from(base64Image, 'base64'));
127
+ const url = result.data?.image_urls?.[0];
128
+ if (url !== undefined) {
129
+ const response = await fetchFn(url);
130
+ if (!response.ok) throw new Error(`Failed to download image from MiniMax: ${response.status}`);
131
+ return new Uint8Array(await response.arrayBuffer());
132
+ }
133
+ throw new Error('No image data in MiniMax response');
134
+ }
135
+
136
+ export const createMinimax: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
137
+ const fetchFn = options?.fetch ?? fetch;
138
+ const env = options?.env ?? process.env;
139
+ const readEnv = (key: string): string => env[key] ?? '';
140
+
141
+ return {
142
+ name: 'minimax',
143
+ defaultModel: () => DEFAULT_MODEL,
144
+ isAvailable: () => readEnv('MINIMAX_API_KEY') !== '',
145
+ supportsReferenceImages: () => true,
146
+ validate(req: ImageRequest): void {
147
+ requestBody(req.prompt, req.model, req);
148
+ },
149
+ async generate(req: ImageRequest): Promise<ImageResult> {
150
+ const apiKey = readEnv('MINIMAX_API_KEY');
151
+ if (apiKey === '')
152
+ throw new Error('MINIMAX_API_KEY is required. Get one from https://platform.minimaxi.com/');
153
+ const body = requestBody(req.prompt, req.model, req);
154
+ const subjectReference = await buildSubjectReference(req.referenceImages ?? []);
155
+ if (subjectReference !== undefined) body.subject_reference = subjectReference;
156
+
157
+ const base = (readEnv('MINIMAX_BASE_URL') || 'https://api.minimaxi.com').replace(/\/+$/g, '');
158
+ const url = base.endsWith('/v1') ? `${base}/image_generation` : `${base}/v1/image_generation`;
159
+ const response = await fetchFn(url, {
160
+ method: 'POST',
161
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
162
+ body: JSON.stringify(body),
163
+ });
164
+ if (!response.ok) {
165
+ throw new Error(`MiniMax API error (${response.status}): ${await response.text()}`);
166
+ }
167
+ const bytes = await extractImageFromResponse((await response.json()) as MinimaxResponse, fetchFn);
168
+ return { bytes, extension: 'jpg', provider: 'minimax', model: req.model };
169
+ },
170
+ };
171
+ };
@@ -0,0 +1,319 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { basename } from 'node:path';
3
+ import { readBytes } from '../bytes';
4
+ import type { ImageProvider, ImageRequest, ImageResult, ProviderFactory, ProviderOptions } from './types';
5
+
6
+ /**
7
+ * OpenAI Images adapter (0115 R1) — port of
8
+ * vendors/baoyu-skills/skills/baoyu-image-gen/scripts/providers/openai.ts
9
+ * (MIT © 2026 Jim Liu). `CliArgs.size` becomes `ImageRequest.width/height`.
10
+ */
11
+
12
+ const DEFAULT_MODEL = 'gpt-image-2';
13
+
14
+ export interface OpenAIImageResponse {
15
+ data: Array<{ url?: string; b64_json?: string }>;
16
+ }
17
+
18
+ /** Size target for openai-native dialect; pick a value only when width+height are known (0115). */
19
+ function explicitSize(req: ImageRequest): string | undefined {
20
+ return req.width !== undefined && req.height !== undefined ? `${req.width}x${req.height}` : undefined;
21
+ }
22
+
23
+ interface AspectAndSizeArgs {
24
+ aspectRatio: string | undefined;
25
+ size: string | undefined;
26
+ quality: 'normal' | '2k' | undefined;
27
+ imageApiDialect?: 'openai-native' | 'ratio-metadata' | undefined;
28
+ }
29
+
30
+ export function parseAspectRatio(ar: string): { width: number; height: number } | null {
31
+ const match = ar.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);
32
+ if (match === null) return null;
33
+ const w = Number.parseFloat(match[1] as string);
34
+ const h = Number.parseFloat(match[2] as string);
35
+ if (w <= 0 || h <= 0) return null;
36
+ return { width: w, height: h };
37
+ }
38
+
39
+ function roundToMultiple(value: number, multiple: number): number {
40
+ return Math.max(multiple, Math.round(value / multiple) * multiple);
41
+ }
42
+
43
+ function buildGptImage2SizeFromAspectRatio(ratio: number, quality: 'normal' | '2k' | undefined): string {
44
+ if (Math.abs(ratio - 1) < 0.1) {
45
+ const edge = quality === '2k' ? 2048 : 1024;
46
+ return `${edge}x${edge}`;
47
+ }
48
+ const targetLongEdge = quality === '2k' ? 2048 : 1024;
49
+ let width: number;
50
+ let height: number;
51
+ if (ratio > 1) {
52
+ width = targetLongEdge;
53
+ height = roundToMultiple(width / ratio, 16);
54
+ } else {
55
+ height = targetLongEdge;
56
+ width = roundToMultiple(height * ratio, 16);
57
+ }
58
+ while (width * height < 655_360) {
59
+ if (ratio > 1) {
60
+ width += 16;
61
+ height = roundToMultiple(width / ratio, 16);
62
+ } else {
63
+ height += 16;
64
+ width = roundToMultiple(height * ratio, 16);
65
+ }
66
+ }
67
+ return `${width}x${height}`;
68
+ }
69
+
70
+ export function getOpenAISize(model: string, ar: string | null, quality: 'normal' | '2k' | undefined): string {
71
+ const isDalle2 = model.includes('dall-e-2');
72
+ const isDalle3 = model.includes('dall-e-3');
73
+ const isGptImage2 = model.includes('gpt-image-2');
74
+
75
+ if (isDalle2) return '1024x1024';
76
+
77
+ if (isGptImage2) {
78
+ const parsed = ar === null ? null : parseAspectRatio(ar);
79
+ const ratio = parsed === null ? 1 : parsed.width / parsed.height;
80
+ return buildGptImage2SizeFromAspectRatio(ratio, quality);
81
+ }
82
+
83
+ const sizes = isDalle3
84
+ ? { square: '1024x1024', landscape: '1792x1024', portrait: '1024x1792' }
85
+ : { square: '1024x1024', landscape: '1536x1024', portrait: '1024x1536' };
86
+ if (ar === null) return sizes.square;
87
+ const parsed = parseAspectRatio(ar);
88
+ if (parsed === null) return sizes.square;
89
+ const ratio = parsed.width / parsed.height;
90
+ if (Math.abs(ratio - 1) < 0.1) return sizes.square;
91
+ if (ratio > 1.5) return sizes.landscape;
92
+ if (ratio < 0.67) return sizes.portrait;
93
+ return sizes.square;
94
+ }
95
+
96
+ function gcd(a: number, b: number): number {
97
+ let x = Math.abs(a);
98
+ let y = Math.abs(b);
99
+ while (y !== 0) {
100
+ const next = x % y;
101
+ x = y;
102
+ y = next;
103
+ }
104
+ return x || 1;
105
+ }
106
+
107
+ export function inferAspectRatioFromSize(size: string | null): string | null {
108
+ if (size === null) return null;
109
+ const parsed = size.match(/^(\d+)\s*[xX]\s*(\d+)$/);
110
+ if (parsed === null) return null;
111
+ const width = Number.parseInt(parsed[1] as string, 10);
112
+ const height = Number.parseInt(parsed[2] as string, 10);
113
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null;
114
+ const divisor = gcd(width, height);
115
+ return `${width / divisor}:${height / divisor}`;
116
+ }
117
+
118
+ export function inferResolutionFromSize(size: string | null): '1K' | '2K' | '4K' | null {
119
+ if (size === null) return null;
120
+ const parsed = size.match(/^(\d+)\s*[xX]\s*(\d+)$/);
121
+ if (parsed === null) return null;
122
+ const width = Number.parseInt(parsed[1] as string, 10);
123
+ const height = Number.parseInt(parsed[2] as string, 10);
124
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null;
125
+ const longestEdge = Math.max(width, height);
126
+ if (longestEdge <= 1024) return '1K';
127
+ if (longestEdge <= 2048) return '2K';
128
+ return '4K';
129
+ }
130
+
131
+ export function getOpenAIAspectRatio(args: { aspectRatio: string | undefined; size: string | undefined }): string {
132
+ return args.aspectRatio ?? inferAspectRatioFromSize(args.size ?? null) ?? '1:1';
133
+ }
134
+
135
+ export function getOpenAIResolution(args: {
136
+ size: string | undefined;
137
+ quality: 'normal' | '2k' | undefined;
138
+ }): '1K' | '2K' | '4K' {
139
+ const inferred = inferResolutionFromSize(args.size ?? null);
140
+ if (inferred !== null) return inferred;
141
+ return args.quality === 'normal' ? '1K' : '2K';
142
+ }
143
+
144
+ type OpenAIQuality = 'standard' | 'hd' | 'medium' | 'high';
145
+
146
+ function getOpenAIQuality(model: string, quality: 'normal' | '2k' | undefined): OpenAIQuality | null {
147
+ if (model.includes('dall-e-3')) return quality === '2k' ? 'hd' : 'standard';
148
+ if (model.includes('gpt-image')) return quality === '2k' ? 'high' : 'medium';
149
+ return null;
150
+ }
151
+
152
+ export function getOrientationFromAspectRatio(ar: string): 'landscape' | 'portrait' | null {
153
+ const parsed = parseAspectRatio(ar);
154
+ if (parsed === null) return null;
155
+ const ratio = parsed.width / parsed.height;
156
+ if (Math.abs(ratio - 1) < 0.1) return null;
157
+ return ratio > 1 ? 'landscape' : 'portrait';
158
+ }
159
+
160
+ export function buildOpenAIGenerationsBody(
161
+ prompt: string,
162
+ model: string,
163
+ args: AspectAndSizeArgs,
164
+ ): Record<string, unknown> {
165
+ if ((args.imageApiDialect ?? 'openai-native') === 'ratio-metadata') {
166
+ const aspectRatio = getOpenAIAspectRatio(args);
167
+ const metadata: Record<string, string> = {
168
+ resolution: getOpenAIResolution({ size: args.size, quality: args.quality }),
169
+ };
170
+ const orientation = getOrientationFromAspectRatio(aspectRatio);
171
+ if (orientation !== null) metadata.orientation = orientation;
172
+ return { model, prompt, size: aspectRatio, metadata };
173
+ }
174
+ const body: Record<string, unknown> = {
175
+ model,
176
+ prompt,
177
+ size: args.size ?? getOpenAISize(model, args.aspectRatio ?? null, args.quality),
178
+ };
179
+ const quality = getOpenAIQuality(model, args.quality);
180
+ if (quality !== null) body.quality = quality;
181
+ return body;
182
+ }
183
+
184
+ /** gpt-image-2 custom size constraints (baoyu validateArgs, px size from width/height). */
185
+ export function validateGptImage2Size(width: number, height: number): void {
186
+ const ratio = Math.max(width, height) / Math.min(width, height);
187
+ if (Math.max(width, height) > 3840)
188
+ throw new Error('gpt-image-2 --size maximum edge length must be 3840px or less.');
189
+ if (width % 16 !== 0 || height % 16 !== 0) {
190
+ throw new Error('gpt-image-2 --size width and height must both be multiples of 16px.');
191
+ }
192
+ if (ratio > 3) throw new Error('gpt-image-2 --size long edge to short edge ratio must not exceed 3:1.');
193
+ const totalPixels = width * height;
194
+ if (totalPixels < 655_360 || totalPixels > 8_294_400) {
195
+ throw new Error('gpt-image-2 --size total pixels must be between 655,360 and 8,294,400.');
196
+ }
197
+ }
198
+
199
+ export function getMimeType(filename: string): string {
200
+ const ext = basename(filename).toLowerCase();
201
+ if (ext.includes('.jpg') || ext.includes('.jpeg')) return 'image/jpeg';
202
+ if (ext.includes('.webp')) return 'image/webp';
203
+ if (ext.includes('.gif')) return 'image/gif';
204
+ return 'image/png';
205
+ }
206
+
207
+ export async function extractImageFromResponse(
208
+ result: OpenAIImageResponse,
209
+ fetchFn: typeof fetch = fetch,
210
+ ): Promise<Uint8Array> {
211
+ const img = result.data[0];
212
+ if (img?.b64_json !== undefined && img.b64_json !== '') {
213
+ return Uint8Array.from(Buffer.from(img.b64_json, 'base64'));
214
+ }
215
+ if (img?.url !== undefined) {
216
+ const imgRes = await fetchFn(img.url);
217
+ if (!imgRes.ok) throw new Error('Failed to download image');
218
+ return new Uint8Array(await imgRes.arrayBuffer());
219
+ }
220
+ throw new Error('No image in response');
221
+ }
222
+
223
+ export const createOpenai: ProviderFactory = (options?: ProviderOptions): ImageProvider => {
224
+ const fetchFn = options?.fetch ?? fetch;
225
+ const env = options?.env ?? process.env;
226
+ const readEnv = (key: string): string => env[key] ?? '';
227
+ const supportsRef = (model: string): boolean => model.includes('gpt-image');
228
+
229
+ return {
230
+ name: 'openai',
231
+ defaultModel: () => DEFAULT_MODEL,
232
+ isAvailable: () => readEnv('OPENAI_API_KEY') !== '',
233
+ supportsReferenceImages: supportsRef,
234
+ validate(req: ImageRequest): void {
235
+ if ((req.referenceImages?.length ?? 0) > 0 && !supportsRef(req.model)) {
236
+ throw new Error(
237
+ `Reference images with OpenAI require GPT Image models. Use --model gpt-image-2 (or another gpt-image model); '${req.model}' is not one.`,
238
+ );
239
+ }
240
+ if (req.width !== undefined && req.height !== undefined && req.model.includes('gpt-image-2')) {
241
+ validateGptImage2Size(req.width, req.height);
242
+ }
243
+ },
244
+ async generate(req: ImageRequest): Promise<ImageResult> {
245
+ const baseURL = readEnv('OPENAI_BASE_URL') || 'https://api.openai.com/v1';
246
+ const apiKey = readEnv('OPENAI_API_KEY');
247
+ if (apiKey === '') {
248
+ throw new Error(
249
+ 'OPENAI_API_KEY is required. Codex/ChatGPT desktop login does not automatically grant OpenAI Images API access.',
250
+ );
251
+ }
252
+
253
+ if (readEnv('OPENAI_IMAGE_USE_CHAT') === 'true') {
254
+ const chatRes = await fetchFn(`${baseURL}/chat/completions`, {
255
+ method: 'POST',
256
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
257
+ body: JSON.stringify({ model: req.model, messages: [{ role: 'user', content: req.prompt }] }),
258
+ });
259
+ if (!chatRes.ok) throw new Error(`OpenAI API error: ${await chatRes.text()}`);
260
+ const content =
261
+ ((await chatRes.json()) as { choices?: Array<{ message?: { content?: string } }> }).choices?.[0]
262
+ ?.message?.content ?? '';
263
+ const match = content.match(/data:image\/[^;]+;base64,([A-Za-z0-9+/=]+)/);
264
+ if (match !== null && match[1] !== undefined) {
265
+ return {
266
+ bytes: Uint8Array.from(Buffer.from(match[1], 'base64')),
267
+ extension: 'png',
268
+ provider: 'openai',
269
+ model: req.model,
270
+ };
271
+ }
272
+ throw new Error('No image found in chat completions response');
273
+ }
274
+
275
+ const refs = req.referenceImages ?? [];
276
+ const args: AspectAndSizeArgs = {
277
+ aspectRatio: req.aspectRatio,
278
+ size: explicitSize(req),
279
+ quality: req.quality,
280
+ };
281
+ const bytes = await (async (): Promise<Uint8Array> => {
282
+ if (refs.length > 0 && (req.model.includes('dall-e-2') || req.model.includes('dall-e-3'))) {
283
+ throw new Error(
284
+ `Reference images with OpenAI require GPT Image models. Use --model gpt-image-2 (or another gpt-image model).`,
285
+ );
286
+ }
287
+ if (refs.length > 0) {
288
+ const size = args.size ?? getOpenAISize(req.model, args.aspectRatio ?? null, args.quality);
289
+ const form = new FormData();
290
+ form.append('model', req.model);
291
+ form.append('prompt', req.prompt);
292
+ form.append('size', size);
293
+ const quality = getOpenAIQuality(req.model, args.quality);
294
+ if (quality !== null && quality !== 'standard' && quality !== 'hd') form.append('quality', quality);
295
+ for (const refPath of refs) {
296
+ const bytes = await readBytes(refPath);
297
+ const filename = basename(refPath);
298
+ form.append('image[]', new Blob([bytes], { type: getMimeType(filename) }), filename);
299
+ }
300
+ const res = await fetchFn(`${baseURL}/images/edits`, {
301
+ method: 'POST',
302
+ headers: { authorization: `Bearer ${apiKey}` },
303
+ body: form,
304
+ });
305
+ if (!res.ok) throw new Error(`OpenAI edits API error: ${await res.text()}`);
306
+ return extractImageFromResponse((await res.json()) as OpenAIImageResponse, fetchFn);
307
+ }
308
+ const res = await fetchFn(`${baseURL}/images/generations`, {
309
+ method: 'POST',
310
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
311
+ body: JSON.stringify(buildOpenAIGenerationsBody(req.prompt, req.model, args)),
312
+ });
313
+ if (!res.ok) throw new Error(`OpenAI API error: ${await res.text()}`);
314
+ return extractImageFromResponse((await res.json()) as OpenAIImageResponse, fetchFn);
315
+ })();
316
+ return { bytes, extension: 'png', provider: 'openai', model: req.model };
317
+ },
318
+ };
319
+ };