@avocadostudio-ai/orchestrator-core 0.9.0 → 0.11.0
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.
- package/dist/chat/chat-pipeline.js +37 -7
- package/dist/chat/prompts.js +2 -2
- package/dist/chat/provider-routing.d.ts +11 -0
- package/dist/chat/provider-routing.js +11 -0
- package/dist/chat/variation-pipeline.js +4 -2
- package/dist/handler/auth.d.ts +22 -2
- package/dist/handler/auth.js +25 -4
- package/dist/handler/create-orchestrator.js +31 -3
- package/dist/http/image-generate-actions.js +6 -4
- package/dist/image/image-helpers.d.ts +17 -0
- package/dist/image/image-helpers.js +25 -0
- package/dist/nlp/deterministic-planner-suggestions.d.ts +22 -0
- package/dist/nlp/deterministic-planner-suggestions.js +39 -3
- package/dist/nlp/deterministic-planner.d.ts +1 -1
- package/dist/nlp/deterministic-planner.js +1 -1
- package/dist/tools/builtins/image-generate.js +4 -2
- package/package.json +3 -3
|
@@ -8,7 +8,7 @@ import { getSiteAssets } from "../state/site-assets.js";
|
|
|
8
8
|
import { loadPendingPlan, savePendingPlan, clearPendingPlan } from "../durable/pending-plan-store.js";
|
|
9
9
|
import { toErrorDetail, isNoEffectiveChangeError, isAlreadyCurrentError, classifyGuardrailError, formatValidationError, isRepairEligibleCategory, buildDeterministicRepairFeedback, validateOperations, applyOpsAtomically, isStructuralOperation, pickFocusBlockId, pickUpdatedSlug } from "../ops/ops-engine.js";
|
|
10
10
|
import { evaluateDestructiveActions } from "../ops/destructive-action-gate.js";
|
|
11
|
-
import { clarificationSuggestions, postEditSuggestions, demoPlanFromMessage, withKeylessNotice, plannerContextPack, compileDeterministicPlan, inferDeterministicIntent, isHighConfidenceDeterministicCase, tryCompoundDeterministicPlan, resolveImageUrlForAltField } from "../nlp/deterministic-planner.js";
|
|
11
|
+
import { clarificationSuggestions, postEditSuggestions, keepDemoExecutable, demoPlanFromMessage, withKeylessNotice, plannerContextPack, compileDeterministicPlan, inferDeterministicIntent, isHighConfidenceDeterministicCase, tryCompoundDeterministicPlan, resolveImageUrlForAltField } from "../nlp/deterministic-planner.js";
|
|
12
12
|
import { generatePlanWithOpenAI, isPlannerOutputError, isStrictJsonResponseEnabled, parseIntentWithOpenAI } from "./planner.js";
|
|
13
13
|
import { isDemoModeEnabled, splitDemoOps, getDemoAllowedBlockTypes } from "../demo-mode.js";
|
|
14
14
|
import { isCancelError as _isCancelError, OperationError, OrchestrationError } from "../errors.js";
|
|
@@ -52,7 +52,7 @@ import { planTranslationChunks, shouldChunkTranslation, generateChunkedTranslati
|
|
|
52
52
|
import { shouldPreferFastModelForMessage, shouldUseLlmIntentRouter, compactPlannerContextPack, minimalPlannerContextPack, shouldUseMinimalPlannerContext, shouldPreferFocusedTranslation, classifyMessageComplexity, isRouterPlanTooShallow, shouldEnableReasoningForMessage } from "./chat-pipeline-context.js";
|
|
53
53
|
import { buildAiInsightChanges, buildMetaChangeLogEntries, buildOpChangeLogEntries, deterministicCreatePagePlan, deterministicDuplicatePagePlan, shouldReturnDeterministicClarification, fmtSlug } from "./chat-pipeline-deterministic.js";
|
|
54
54
|
import { getValueAtPath, setValueAtPath, deleteValueAtPath, blockSupportsImageAtPath, detectImageOps, rewriteAddBlockToChildImageUpdate, withUnsplashHeroImage, resolveHeroImageForCreatePage } from "./chat-pipeline-image.js";
|
|
55
|
-
import { resolveEffectiveProvider, resolveModelKeyForProvider, resolvePlannerSource } from "./provider-routing.js";
|
|
55
|
+
import { resolveEffectiveProvider, resolveModelKeyForProvider, resolvePlannerSource, DEMO_MODEL_LABEL } from "./provider-routing.js";
|
|
56
56
|
import { runVariationPipeline, getCachedVariations } from "./variation-pipeline.js";
|
|
57
57
|
import { validateAndStripHallucinatedProps } from "./hallucination-validator.js";
|
|
58
58
|
import { validateChangelogCoverage } from "./changelog-coverage-validator.js";
|
|
@@ -183,6 +183,19 @@ let generatePlanWithAnthropicImpl = generatePlanWithAnthropic;
|
|
|
183
183
|
export function setGeneratePlanWithAnthropicForTests(fn) {
|
|
184
184
|
generatePlanWithAnthropicImpl = fn ?? generatePlanWithAnthropic;
|
|
185
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Suggestions, narrowed to what will answer when there is no key.
|
|
188
|
+
*
|
|
189
|
+
* A no-op the moment any provider is configured — with a model behind the chat,
|
|
190
|
+
* every phrasing these generators produce is answerable, which is what they
|
|
191
|
+
* were written against. Without one the answer comes from a substring matcher
|
|
192
|
+
* that knows a handful of literal phrases, and the rest are dead buttons.
|
|
193
|
+
*/
|
|
194
|
+
function usableSuggestions(suggestions, source, slug, body) {
|
|
195
|
+
if (source !== "demo")
|
|
196
|
+
return suggestions;
|
|
197
|
+
return keepDemoExecutable(suggestions, slug, body.activeBlockId, body.activeBlockType);
|
|
198
|
+
}
|
|
186
199
|
let demoPlanFromMessageImpl = demoPlanFromMessage;
|
|
187
200
|
export function setDemoPlanFromMessageForTests(fn) {
|
|
188
201
|
demoPlanFromMessageImpl = fn ?? demoPlanFromMessage;
|
|
@@ -275,7 +288,9 @@ export async function runChatPipeline(ctx, body, options) {
|
|
|
275
288
|
changes: [],
|
|
276
289
|
previewVersion: versions.get(body.session) ?? 0,
|
|
277
290
|
plannerSource: ctx.availableProviders.length > 0 ? ctx.availableProviders[0] : "demo",
|
|
278
|
-
modelUsed: ctx.
|
|
291
|
+
modelUsed: ctx.availableProviders.length > 0
|
|
292
|
+
? ctx.modelLookup[defaultProvider][defaultModelKey]
|
|
293
|
+
: DEMO_MODEL_LABEL,
|
|
279
294
|
modelKey: defaultModelKey
|
|
280
295
|
}
|
|
281
296
|
};
|
|
@@ -481,8 +496,23 @@ export async function runChatPipeline(ctx, body, options) {
|
|
|
481
496
|
shouldPreferFastModelForMessage(plannerMessage)
|
|
482
497
|
? "fast"
|
|
483
498
|
: baseModelKey;
|
|
484
|
-
const modelUsed = ctx.modelLookup[provider][modelKey];
|
|
485
499
|
const plannerSource = resolvePlannerSource(provider);
|
|
500
|
+
/*
|
|
501
|
+
* What actually answered, not what would have.
|
|
502
|
+
*
|
|
503
|
+
* `plannerSource === "demo"` means no provider key of any kind is
|
|
504
|
+
* configured, so no model can run — yet this reported the configured
|
|
505
|
+
* lookup's default anyway, and every keyless reply came back
|
|
506
|
+
* `"modelUsed":"gpt-4o"`. The editor renders a per-message model chip from
|
|
507
|
+
* this, so someone who had deliberately not signed up for anything watched
|
|
508
|
+
* an OpenAI model take credit for nine words of regex output.
|
|
509
|
+
*
|
|
510
|
+
* Narrower than it looks: a *deterministic* plan produced while a key IS
|
|
511
|
+
* configured keeps naming the model, because that model would genuinely have
|
|
512
|
+
* run had the rules not matched first — see `plannerTier` below. The case
|
|
513
|
+
* being corrected is the one where there is nothing to name.
|
|
514
|
+
*/
|
|
515
|
+
const modelUsed = plannerSource === "demo" ? DEMO_MODEL_LABEL : ctx.modelLookup[provider][modelKey];
|
|
486
516
|
// Auto-escalate to extended thinking for complex/ambiguous Anthropic prompts.
|
|
487
517
|
// Only applies when provider = anthropic (other providers ignore `thinking`).
|
|
488
518
|
// User can disable with CHAT_AUTO_REASONING=0.
|
|
@@ -1095,7 +1125,7 @@ export async function runChatPipeline(ctx, body, options) {
|
|
|
1095
1125
|
summary: "I need one more detail before applying this safely.",
|
|
1096
1126
|
changes: [],
|
|
1097
1127
|
mentionedSlugs: [effectiveSlug],
|
|
1098
|
-
suggestions: clarificationSuggestions({ body, current, selected }),
|
|
1128
|
+
suggestions: usableSuggestions(clarificationSuggestions({ body, current, selected }), args.source, effectiveSlug, body),
|
|
1099
1129
|
previewVersion: versions.get(body.session) ?? 0,
|
|
1100
1130
|
plannerSource: args.source,
|
|
1101
1131
|
modelUsed,
|
|
@@ -1719,7 +1749,7 @@ export async function runChatPipeline(ctx, body, options) {
|
|
|
1719
1749
|
summary: resolvedPlan.summary_for_user,
|
|
1720
1750
|
changes: resolvedPlan.change_log,
|
|
1721
1751
|
mentionedSlugs: collectMentionedSlugsFromPlan(resolvedPlan, effectiveSlug),
|
|
1722
|
-
suggestions: resolvedPlan.suggested_next_actions ?? clarificationSuggestions({ body, current, selected }),
|
|
1752
|
+
suggestions: usableSuggestions(resolvedPlan.suggested_next_actions ?? clarificationSuggestions({ body, current, selected }), source, effectiveSlug, body),
|
|
1723
1753
|
previewVersion: versions.get(body.session) ?? 0,
|
|
1724
1754
|
plannerSource: source,
|
|
1725
1755
|
modelUsed,
|
|
@@ -2623,7 +2653,7 @@ export async function runChatPipeline(ctx, body, options) {
|
|
|
2623
2653
|
summary: futureToPastTense(resolvedPlan.summary_for_user),
|
|
2624
2654
|
changes: [...opChangeLogEntries, ...metaChangeLogEntries, ...aiInsightChanges, ...skippedSummary],
|
|
2625
2655
|
mentionedSlugs: collectMentionedSlugsFromPlan(resolvedPlan, updatedSlug ?? effectiveSlug),
|
|
2626
|
-
suggestions: resolvedPlan.suggested_next_actions ?? postEditSuggestions({ plan: resolvedPlan, current, body }),
|
|
2656
|
+
suggestions: usableSuggestions(resolvedPlan.suggested_next_actions ?? postEditSuggestions({ plan: resolvedPlan, current, body }), plannerSource, updatedSlug ?? effectiveSlug, body),
|
|
2627
2657
|
previewVersion,
|
|
2628
2658
|
focusBlockId,
|
|
2629
2659
|
updatedSlug,
|
package/dist/chat/prompts.js
CHANGED
|
@@ -134,7 +134,7 @@ function buildLightweightPlannerPrompt(opts) {
|
|
|
134
134
|
RULE_ICON_FORMAT,
|
|
135
135
|
"Use future tense in summary_for_user and change_log — your output streams to the user while the plan is still being generated, before any ops have been applied. Say 'Will update the heading…' or 'Will replace the Hero image…', never 'Updated…' or 'Updating…'. The system flips to past tense automatically once ops are applied.",
|
|
136
136
|
"For edit_plan: summary_for_user must be ONE short sentence (max ~20 words).",
|
|
137
|
-
"After planning ops, include suggested_next_actions: 2-4 short imperative phrases the user could type next (max 6 words each). Every suggestion must be an action the user can perform inside this editor (editing content, adding/removing sections, changing images, rewriting copy) — restricted to the block types listed in the block catalogue provided in context (Hero, FeatureGrid, Testimonials, FAQAccordion, CTA, Card, CardGrid, RichText, TwoColumn, Banner, Carousel, Embed, Footer, Gallery, Quote, SiteHeader, Stats, Table, Tabs, Video). NEVER suggest unsupported features like forms, email capture, contact forms, subscribe boxes, newsletter signups, popups, modals, or anything requiring custom code. Never suggest actions outside the editor's scope such as A/B testing, analytics, performance monitoring, user research, or marketing strategy. When the plan contains exactly one update_props op that changes a text field, the first 1-2 suggestions MUST be refinements of that same field (e.g. 'Make it shorter', 'Try a bolder tone', '
|
|
137
|
+
"After planning ops, include suggested_next_actions: 2-4 short imperative phrases the user could type next (max 6 words each). Every suggestion must be an action the user can perform inside this editor (editing content, adding/removing sections, changing images, rewriting copy) — restricted to the block types listed in the block catalogue provided in context (Hero, FeatureGrid, Testimonials, FAQAccordion, CTA, Card, CardGrid, RichText, TwoColumn, Banner, Carousel, Embed, Footer, Gallery, Quote, SiteHeader, Stats, Table, Tabs, Video). NEVER suggest unsupported features like forms, email capture, contact forms, subscribe boxes, newsletter signups, popups, modals, or anything requiring custom code. Never suggest actions outside the editor's scope such as A/B testing, analytics, performance monitoring, user research, or marketing strategy. When the plan contains exactly one update_props op that changes a text field, the first 1-2 suggestions MUST be refinements of that same field (e.g. 'Make it shorter', 'Try a bolder tone', 'Use a warmer opening'). Remaining suggestions can target neighboring fields or blocks. NEVER suggest undoing, reverting, or restoring earlier content. You cannot read the undo history, so such a suggestion resolves to either a refusal or an invented replacement presented as the original — the editor's own Undo button is the affordance for that, and it already works.",
|
|
138
138
|
opts.selectedBlockId.length > 0
|
|
139
139
|
? `Selected block is ${opts.selectedBlockId}. Target only this block in ops when the request edits the current page. IGNORE this selection when the request operates on a different scope — creating, duplicating, renaming, removing, or moving a page; editing site config; or naming a different page — and emit ops only for the requested scope. Never add bonus ops on the selected block to satisfy this rule.`
|
|
140
140
|
: "Respect explicit user target references when present.",
|
|
@@ -250,7 +250,7 @@ function sectionVoice(opts, hasNativeTools) {
|
|
|
250
250
|
if (hasNativeTools) {
|
|
251
251
|
lines.push("For edit_plan intent: summary_for_user must be ONE short sentence (max ~20 words) describing what the plan will do. Do NOT elaborate, explain why, or describe the content being added — let change_log carry the detail. Bad: 'Updated the hero heading with a punchier tone.' Good: 'Will add a **text section** about blueberry varieties after the features grid.'", "change_log coverage is MANDATORY: emit exactly one change_log entry per op, in the same order as ops[], describing what that specific op does. If ops has N entries, change_log must have N entries — never cluster multiple ops into one entry, never skip an op, never leave an op undescribed. The user reads change_log to decide whether to approve; a missing entry is a silent bait-and-switch.", "change_log entries should add specific detail NOT already in summary_for_user — e.g. list the actual content, items, or values being set. Do not paraphrase the summary.");
|
|
252
252
|
}
|
|
253
|
-
lines.push("In summary_for_user, use simple markdown for readability: **bold** for key terms or labels, and bullet lists (- item) when listing multiple items, recommendations, or observations. Keep it scannable — avoid walls of text.", "When rewriting text, return plain text unless the prop is a rich-text prop (see below) or the user explicitly asks for markdown formatting. Do not wrap the entire rewrite in **bold** markers.", RULE_RICH_TEXT_PROPS, "For copy in German or similar long-compound languages, insert soft hyphen opportunities in long compounds where helpful for responsive line wrapping. Use the Unicode soft hyphen character (U+00AD), never HTML entities like ­ or ­.", opts.provider !== "openai" ? BLOCK_NAME_PRIVACY_ANTHROPIC : BLOCK_NAME_PRIVACY_OPENAI, "", "### suggested_next_actions", "2-4 short imperative phrases the user could type next (max 6 words each). Each MUST be a logical follow-up to the specific change just made — not a generic action. NEVER suggest 'Open /X' or any navigation to a page that the current plan is creating, duplicating, or otherwise still pending — the page won't exist until the user approves the plan, and there is no special navigation chip handler (suggestions are sent verbatim as the next chat command). Suggest plan refinements instead (e.g. 'Use a punchier hero headline', 'Add a card grid for spotlights', 'Drop the FAQ section'). Ask yourself: 'what would the user likely want to do next given THIS edit?' When the plan contains exactly one update_props op that changes a text field, the first 1-2 suggestions MUST be refinements of that same field (e.g. 'Make it shorter', 'Try a bolder tone', '
|
|
253
|
+
lines.push("In summary_for_user, use simple markdown for readability: **bold** for key terms or labels, and bullet lists (- item) when listing multiple items, recommendations, or observations. Keep it scannable — avoid walls of text.", "When rewriting text, return plain text unless the prop is a rich-text prop (see below) or the user explicitly asks for markdown formatting. Do not wrap the entire rewrite in **bold** markers.", RULE_RICH_TEXT_PROPS, "For copy in German or similar long-compound languages, insert soft hyphen opportunities in long compounds where helpful for responsive line wrapping. Use the Unicode soft hyphen character (U+00AD), never HTML entities like ­ or ­.", opts.provider !== "openai" ? BLOCK_NAME_PRIVACY_ANTHROPIC : BLOCK_NAME_PRIVACY_OPENAI, "", "### suggested_next_actions", "2-4 short imperative phrases the user could type next (max 6 words each). Each MUST be a logical follow-up to the specific change just made — not a generic action. NEVER suggest 'Open /X' or any navigation to a page that the current plan is creating, duplicating, or otherwise still pending — the page won't exist until the user approves the plan, and there is no special navigation chip handler (suggestions are sent verbatim as the next chat command). Suggest plan refinements instead (e.g. 'Use a punchier hero headline', 'Add a card grid for spotlights', 'Drop the FAQ section'). Ask yourself: 'what would the user likely want to do next given THIS edit?' When the plan contains exactly one update_props op that changes a text field, the first 1-2 suggestions MUST be refinements of that same field (e.g. 'Make it shorter', 'Try a bolder tone', 'Use a warmer opening'). For example, after rewriting stats labels, suggest refining the same section ('Make the numbers bigger', 'Add a stat about X') — not unrelated actions like 'Change title' or 'Add a Testimonials section'. For needs_clarification, suggest the most likely concrete answers. Omit suggested_next_actions entirely if no contextual follow-up is obvious. Every suggestion must be an action the user can perform inside this editor — restricted to the block types listed in blockContracts / blockCatalogue for THIS site, or SEO/site-config edits. Never suggest adding a section this site has no block for; the catalogue is the whole list, not a sample of a larger one. NEVER suggest unsupported features: no forms, no email capture, no contact forms, no subscribe boxes, no newsletter signups, no popups/modals, no chat widgets, no live video, no payment/checkout — these require custom code the editor cannot produce. Never suggest actions outside the editor's scope such as A/B testing, analytics, performance monitoring, user research, or marketing strategy. NEVER suggest undoing, reverting, or restoring earlier content. You cannot read the undo history, so such a suggestion resolves to either a refusal or an invented replacement presented as the original — the editor's own Undo button is the affordance for that, and it already works.");
|
|
254
254
|
return lines;
|
|
255
255
|
}
|
|
256
256
|
/*
|
|
@@ -12,3 +12,14 @@ export declare function resolveModelKeyForProvider(args: {
|
|
|
12
12
|
defaultModelKey?: ModelKey;
|
|
13
13
|
}): ModelKey;
|
|
14
14
|
export declare function resolvePlannerSource(provider: AIProvider): PlannerSource;
|
|
15
|
+
/**
|
|
16
|
+
* What to report as the model when `resolvePlannerSource` said `"demo"`.
|
|
17
|
+
*
|
|
18
|
+
* A string rather than `undefined` because every consumer of `modelUsed` — the
|
|
19
|
+
* editor's per-message chip, the telemetry row, the debug panel — already
|
|
20
|
+
* treats absence as "unknown" and renders nothing. "Unknown" is not what this
|
|
21
|
+
* is. The reply came from a named thing that is not a model, and saying so is
|
|
22
|
+
* the whole point: it is the one place a keyless session is told, in passing,
|
|
23
|
+
* what it is running on.
|
|
24
|
+
*/
|
|
25
|
+
export declare const DEMO_MODEL_LABEL = "demo planner (no API key)";
|
|
@@ -25,3 +25,14 @@ export function resolvePlannerSource(provider) {
|
|
|
25
25
|
? "gemini"
|
|
26
26
|
: "demo";
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* What to report as the model when `resolvePlannerSource` said `"demo"`.
|
|
30
|
+
*
|
|
31
|
+
* A string rather than `undefined` because every consumer of `modelUsed` — the
|
|
32
|
+
* editor's per-message chip, the telemetry row, the debug panel — already
|
|
33
|
+
* treats absence as "unknown" and renders nothing. "Unknown" is not what this
|
|
34
|
+
* is. The reply came from a named thing that is not a model, and saying so is
|
|
35
|
+
* the whole point: it is the one place a keyless session is told, in passing,
|
|
36
|
+
* what it is running on.
|
|
37
|
+
*/
|
|
38
|
+
export const DEMO_MODEL_LABEL = "demo planner (no API key)";
|
|
@@ -10,7 +10,7 @@ import { openAIChatOptionsForModel } from "./planner.js";
|
|
|
10
10
|
import { extractJsonObject } from "../nlp/plan-normalizer.js";
|
|
11
11
|
import { extractUsage, estimateUsd } from "../telemetry/usage.js";
|
|
12
12
|
import { anthropicSystemPromptWithCache } from "./anthropic-cache.js";
|
|
13
|
-
import { resolveEffectiveProvider, resolveModelKeyForProvider, resolvePlannerSource } from "./provider-routing.js";
|
|
13
|
+
import { resolveEffectiveProvider, resolveModelKeyForProvider, resolvePlannerSource, DEMO_MODEL_LABEL } from "./provider-routing.js";
|
|
14
14
|
import { deriveVariationImageIntent, buildVariationImageQuery, buildVariationImagePrompt, generateVariationImageWithOpenAI, generateVariationImageWithGemini, resolveUnsplashImage } from "../image/image-helpers.js";
|
|
15
15
|
import { firstUrlFromText, resolveEffectiveSlug } from "./chat-pipeline.js";
|
|
16
16
|
import { resolveDistinctUnsplashImage } from "../variation-images.js";
|
|
@@ -656,9 +656,11 @@ async function prepareTextVariations(ctx, body) {
|
|
|
656
656
|
modelLookup: ctx.modelLookup,
|
|
657
657
|
defaultModelKey: process.env.OPENAI_MODEL_KEY ?? "balanced"
|
|
658
658
|
});
|
|
659
|
-
const modelUsed = ctx.modelLookup[provider][modelKey];
|
|
660
659
|
const count = requestedVariationCount(contextualMessage);
|
|
661
660
|
const plannerSource = resolvePlannerSource(provider);
|
|
661
|
+
/* Same correction as the chat pipeline: with no key, nothing in the lookup
|
|
662
|
+
* ran, so naming one credits a model for a deterministic result. */
|
|
663
|
+
const modelUsed = plannerSource === "demo" ? DEMO_MODEL_LABEL : ctx.modelLookup[provider][modelKey];
|
|
662
664
|
let variations = [];
|
|
663
665
|
let generatorUsage;
|
|
664
666
|
if (plannerSource === "anthropic") {
|
package/dist/handler/auth.d.ts
CHANGED
|
@@ -59,8 +59,28 @@ export declare function hasConfiguredCredential(): boolean;
|
|
|
59
59
|
* captured at module load is a value from before the host configured anything.
|
|
60
60
|
*/
|
|
61
61
|
export declare function resolveAuth(auth: OrchestratorAuth | undefined): ResolvedAuth;
|
|
62
|
-
/**
|
|
63
|
-
|
|
62
|
+
/**
|
|
63
|
+
* The reason a `closed` mount refuses, in the 401 itself.
|
|
64
|
+
*
|
|
65
|
+
* Refusing anonymously is right when a credential exists and the caller did
|
|
66
|
+
* not present it — "which header did I get wrong" is not the server's to
|
|
67
|
+
* answer. `closed` is not that case. Nobody holds a credential because none was
|
|
68
|
+
* configured, so there is no attacker to withhold this from: the only person
|
|
69
|
+
* who can act on it is the operator, staring at a 401 on their own deployment.
|
|
70
|
+
*
|
|
71
|
+
* Withholding it cost a clean-room run its whole production evaluation. Every
|
|
72
|
+
* route answered `{"error":"unauthorized"}`, `/auth/status` answered
|
|
73
|
+
* `gateEnabled: false`, and nothing anywhere named an environment variable.
|
|
74
|
+
*/
|
|
75
|
+
export declare const CLOSED_HINT: string;
|
|
76
|
+
/**
|
|
77
|
+
* The one body shape the editor's fetch shim recognises as "re-prompt".
|
|
78
|
+
*
|
|
79
|
+
* `reason` is additive and present only for a misconfiguration the caller
|
|
80
|
+
* cannot have caused; `error` keeps its exact value either way, because that
|
|
81
|
+
* is the field the shim matches on.
|
|
82
|
+
*/
|
|
83
|
+
export declare function unauthorizedResponse(cors: Record<string, string>, reason?: string): Response;
|
|
64
84
|
export interface AuthCheckResult {
|
|
65
85
|
/** Non-null means refuse and return this. */
|
|
66
86
|
response: Response | null;
|
package/dist/handler/auth.js
CHANGED
|
@@ -76,9 +76,30 @@ export function resolveAuth(auth) {
|
|
|
76
76
|
}
|
|
77
77
|
return { mode: "open-dev", reason: "no credential configured and NODE_ENV is not production — open" };
|
|
78
78
|
}
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
/**
|
|
80
|
+
* The reason a `closed` mount refuses, in the 401 itself.
|
|
81
|
+
*
|
|
82
|
+
* Refusing anonymously is right when a credential exists and the caller did
|
|
83
|
+
* not present it — "which header did I get wrong" is not the server's to
|
|
84
|
+
* answer. `closed` is not that case. Nobody holds a credential because none was
|
|
85
|
+
* configured, so there is no attacker to withhold this from: the only person
|
|
86
|
+
* who can act on it is the operator, staring at a 401 on their own deployment.
|
|
87
|
+
*
|
|
88
|
+
* Withholding it cost a clean-room run its whole production evaluation. Every
|
|
89
|
+
* route answered `{"error":"unauthorized"}`, `/auth/status` answered
|
|
90
|
+
* `gateEnabled: false`, and nothing anywhere named an environment variable.
|
|
91
|
+
*/
|
|
92
|
+
export const CLOSED_HINT = "This orchestrator is running with NODE_ENV=production and no credential, so every request is refused. " +
|
|
93
|
+
"Set ACCESS_PASSWORD_HASH or ORCHESTRATOR_ACCESS_TOKEN, or pass config.auth to createOrchestrator().";
|
|
94
|
+
/**
|
|
95
|
+
* The one body shape the editor's fetch shim recognises as "re-prompt".
|
|
96
|
+
*
|
|
97
|
+
* `reason` is additive and present only for a misconfiguration the caller
|
|
98
|
+
* cannot have caused; `error` keeps its exact value either way, because that
|
|
99
|
+
* is the field the shim matches on.
|
|
100
|
+
*/
|
|
101
|
+
export function unauthorizedResponse(cors, reason) {
|
|
102
|
+
return new Response(JSON.stringify({ error: "unauthorized", ...(reason ? { reason } : {}) }), {
|
|
82
103
|
status: 401,
|
|
83
104
|
headers: { "content-type": "application/json", ...cors }
|
|
84
105
|
});
|
|
@@ -95,7 +116,7 @@ export async function checkAuth(args) {
|
|
|
95
116
|
return { response: null, context: null };
|
|
96
117
|
if (resolved.mode === "closed") {
|
|
97
118
|
log.error({ path }, `[auth] refused — ${resolved.reason}`);
|
|
98
|
-
return { response: unauthorizedResponse(cors), context: null };
|
|
119
|
+
return { response: unauthorizedResponse(cors, CLOSED_HINT), context: null };
|
|
99
120
|
}
|
|
100
121
|
if (resolved.mode === "token") {
|
|
101
122
|
if (isValidAccessToken(extractAccessToken(request)))
|
|
@@ -52,7 +52,7 @@ import { resolveCapabilities } from "../cms/adapter.js";
|
|
|
52
52
|
import { mediaSourceFromUnknown } from "../cms/media-sources.js";
|
|
53
53
|
import { isAccessGateEnabled, mintAccessToken, verifyAccessPassword } from "../http/access-tokens.js";
|
|
54
54
|
import { declareLibraryMount, observeLibraryMount } from "./library-mount.js";
|
|
55
|
-
import { checkAuth, resolveAuth } from "./auth.js";
|
|
55
|
+
import { checkAuth, resolveAuth, CLOSED_HINT } from "./auth.js";
|
|
56
56
|
import { setSiteAssetLister, invalidateSiteAssets } from "../state/site-assets.js";
|
|
57
57
|
const defaultModelLookup = () => ({
|
|
58
58
|
openai: {
|
|
@@ -533,8 +533,27 @@ export function createOrchestrator(config = {}) {
|
|
|
533
533
|
* editor to prompt for one it cannot verify would trap the user in a form
|
|
534
534
|
* that never succeeds. Such a host authenticates the browser its own way,
|
|
535
535
|
* before the editor ever loads.
|
|
536
|
+
*
|
|
537
|
+
* `mode` answers the question `gateEnabled` was being made to stand in
|
|
538
|
+
* for, and could not: "will my requests be accepted?". The two come apart
|
|
539
|
+
* in exactly one state, and it is the worst one. A `closed` mount — built
|
|
540
|
+
* for production with no credential — has no password gate, so
|
|
541
|
+
* `gateEnabled` is false and the editor opens without prompting, while
|
|
542
|
+
* every route behind this one answers 401. The editor then renders itself
|
|
543
|
+
* around a site it cannot read, with no password box and no way to reach
|
|
544
|
+
* one; a clean-room run hit exactly that and had nothing to go on.
|
|
545
|
+
*
|
|
546
|
+
* So the state is named. `/auth/status` is public precisely so a client
|
|
547
|
+
* can ask before it holds a credential, and `closed` is a misconfiguration
|
|
548
|
+
* rather than a secret — nobody holds a credential for this mount, so
|
|
549
|
+
* there is no one to withhold it from.
|
|
536
550
|
*/
|
|
537
|
-
|
|
551
|
+
const resolvedAuth = resolveAuth(config.auth);
|
|
552
|
+
return jsonResponse({
|
|
553
|
+
gateEnabled: isAccessGateEnabled(),
|
|
554
|
+
mode: resolvedAuth.mode,
|
|
555
|
+
...(resolvedAuth.mode === "closed" ? { reason: CLOSED_HINT } : {})
|
|
556
|
+
}, { cors });
|
|
538
557
|
}
|
|
539
558
|
if (request.method === "POST" && path === "/auth/verify") {
|
|
540
559
|
if (!isAccessGateEnabled()) {
|
|
@@ -948,10 +967,19 @@ export function createOrchestrator(config = {}) {
|
|
|
948
967
|
}
|
|
949
968
|
};
|
|
950
969
|
if (!runtime.adapter?.onPublish) {
|
|
970
|
+
/*
|
|
971
|
+
* Two audiences, and they had been given the same sentence. `reason` is
|
|
972
|
+
* for whatever called the API; `message` is what the editor prints as
|
|
973
|
+
* the summary under a green tick, so a first-run user read the words
|
|
974
|
+
* "adapter has no onPublish; publish is a no-op" as the report on the
|
|
975
|
+
* button they had just pressed.
|
|
976
|
+
*/
|
|
951
977
|
const reason = "adapter has no onPublish; publish is a no-op";
|
|
978
|
+
const message = "Nothing was written — this site's content adapter is read-only. Your edits are " +
|
|
979
|
+
"still here as a draft; to publish them, give the adapter somewhere to write.";
|
|
952
980
|
record(true, "Nothing written — the adapter has no onPublish");
|
|
953
981
|
return jsonResponse({
|
|
954
|
-
...publishEnvelope(body.session, slugs,
|
|
982
|
+
...publishEnvelope(body.session, slugs, message),
|
|
955
983
|
ok: true,
|
|
956
984
|
written: false,
|
|
957
985
|
count: pages.length,
|
|
@@ -27,7 +27,7 @@ import { randomUUID } from "node:crypto";
|
|
|
27
27
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
28
28
|
import { resolve } from "node:path";
|
|
29
29
|
import OpenAI from "openai";
|
|
30
|
-
import { getGeminiClient, getGeminiImageModel, saveGeneratedImage, GEMINI_ASPECT_RATIOS } from "../image/image-helpers.js";
|
|
30
|
+
import { getGeminiClient, getGeminiImageModel, resolveImageProvider, saveGeneratedImage, GEMINI_ASPECT_RATIOS } from "../image/image-helpers.js";
|
|
31
31
|
import { openAIChatOptionsForModel } from "../chat/planner.js";
|
|
32
32
|
import { toErrorDetail } from "../errors.js";
|
|
33
33
|
/**
|
|
@@ -120,12 +120,14 @@ export async function generateImageAction(body, deps) {
|
|
|
120
120
|
return { code: 400, body: { error: "prompt is required" } };
|
|
121
121
|
const requestedProvider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
|
|
122
122
|
const requestedModel = typeof body.model === "string" ? body.model.trim() : "";
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
/* Resolved against the keys this process holds, not just against what was
|
|
124
|
+
* asked for — see resolveImageProvider. `hasOpenAI || hasGemini` is checked
|
|
125
|
+
* above, so this is non-null here. */
|
|
126
|
+
const provider = resolveImageProvider(requestedProvider) ?? "openai";
|
|
125
127
|
const alt = prompt.slice(0, 200);
|
|
126
128
|
const store = storeOf(deps);
|
|
127
129
|
try {
|
|
128
|
-
if (provider === "gemini"
|
|
130
|
+
if (provider === "gemini") {
|
|
129
131
|
const url = await generateWithGemini({ prompt, aspectRatio: body.aspectRatio, model: requestedModel || undefined }, deps, store);
|
|
130
132
|
if (!url)
|
|
131
133
|
return { code: 502, body: { error: "Gemini image generation returned no data" } };
|
|
@@ -73,6 +73,23 @@ export declare function saveGeminiInlineImage(parts: Array<{
|
|
|
73
73
|
} | null>;
|
|
74
74
|
export declare const GEMINI_ASPECT_RATIOS: Record<string, string>;
|
|
75
75
|
export declare function getGeminiClient(): Promise<any>;
|
|
76
|
+
export type ImageProvider = "openai" | "gemini";
|
|
77
|
+
/**
|
|
78
|
+
* Which image backend this process can actually reach, given the keys it holds.
|
|
79
|
+
*
|
|
80
|
+
* `IMAGE_GEN_PROVIDER` (and a per-request hint) says which backend is *wanted*;
|
|
81
|
+
* this says which one can answer. The two were previously conflated, and the
|
|
82
|
+
* fallback ran in one direction only — a gemini request without a Google key
|
|
83
|
+
* fell through to OpenAI, but an OpenAI request without an OpenAI key did not
|
|
84
|
+
* fall through to Gemini. Since `IMAGE_GEN_PROVIDER` defaults to `openai`, a
|
|
85
|
+
* deployment funding only `GOOGLE_GENAI_API_KEY` took that second branch on
|
|
86
|
+
* every request: `/status/planner` advertised `imageGenerate: true` (it is true
|
|
87
|
+
* for *either* key), the editor showed the Generate tab, and every generation
|
|
88
|
+
* came back "OPENAI_API_KEY not configured".
|
|
89
|
+
*
|
|
90
|
+
* Returns null when neither key is set, which is the caller's 503.
|
|
91
|
+
*/
|
|
92
|
+
export declare function resolveImageProvider(requested?: string): ImageProvider | null;
|
|
76
93
|
export declare function getGeminiImageModel(): string;
|
|
77
94
|
export declare function generateVariationImageWithGemini(args: {
|
|
78
95
|
prompt: string;
|
|
@@ -310,6 +310,31 @@ export async function getGeminiClient() {
|
|
|
310
310
|
_geminiClient = new GoogleGenAI({ apiKey: process.env.GOOGLE_GENAI_API_KEY });
|
|
311
311
|
return _geminiClient;
|
|
312
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Which image backend this process can actually reach, given the keys it holds.
|
|
315
|
+
*
|
|
316
|
+
* `IMAGE_GEN_PROVIDER` (and a per-request hint) says which backend is *wanted*;
|
|
317
|
+
* this says which one can answer. The two were previously conflated, and the
|
|
318
|
+
* fallback ran in one direction only — a gemini request without a Google key
|
|
319
|
+
* fell through to OpenAI, but an OpenAI request without an OpenAI key did not
|
|
320
|
+
* fall through to Gemini. Since `IMAGE_GEN_PROVIDER` defaults to `openai`, a
|
|
321
|
+
* deployment funding only `GOOGLE_GENAI_API_KEY` took that second branch on
|
|
322
|
+
* every request: `/status/planner` advertised `imageGenerate: true` (it is true
|
|
323
|
+
* for *either* key), the editor showed the Generate tab, and every generation
|
|
324
|
+
* came back "OPENAI_API_KEY not configured".
|
|
325
|
+
*
|
|
326
|
+
* Returns null when neither key is set, which is the caller's 503.
|
|
327
|
+
*/
|
|
328
|
+
export function resolveImageProvider(requested) {
|
|
329
|
+
const hasOpenAI = Boolean(process.env.OPENAI_API_KEY);
|
|
330
|
+
const hasGemini = Boolean(process.env.GOOGLE_GENAI_API_KEY);
|
|
331
|
+
if (!hasOpenAI && !hasGemini)
|
|
332
|
+
return null;
|
|
333
|
+
const want = (requested?.trim().toLowerCase() || process.env.IMAGE_GEN_PROVIDER?.trim().toLowerCase() || "openai");
|
|
334
|
+
if (want === "gemini")
|
|
335
|
+
return hasGemini ? "gemini" : "openai";
|
|
336
|
+
return hasOpenAI ? "openai" : "gemini";
|
|
337
|
+
}
|
|
313
338
|
export function getGeminiImageModel() {
|
|
314
339
|
return process.env.GOOGLE_GENAI_IMAGE_MODEL?.trim() || "gemini-3.1-flash-lite-image";
|
|
315
340
|
}
|
|
@@ -75,4 +75,26 @@ export declare function blockContractsSummary(manifest?: BlockManifest): Record<
|
|
|
75
75
|
*/
|
|
76
76
|
export declare const KEYLESS_PLANNER_NOTICE: string;
|
|
77
77
|
export declare function withKeylessNotice(plan: EditPlan): EditPlan;
|
|
78
|
+
/**
|
|
79
|
+
* Drop the suggestions this planner could not act on.
|
|
80
|
+
*
|
|
81
|
+
* `postEditSuggestions` and `clarificationSuggestions` are written against the
|
|
82
|
+
* product — "Edit Banner", "Move FAQ", "Add author photos" — because with a key
|
|
83
|
+
* configured a model answers them, and it can. With no key the answer comes
|
|
84
|
+
* from `demoPlanFromMessage`, a short list of English substring matchers, and
|
|
85
|
+
* almost none of those phrasings is on it. A clean-room first run clicked six
|
|
86
|
+
* follow-up chips against a keyless scaffold; five produced zero operations and
|
|
87
|
+
* the same paragraph about adding a key.
|
|
88
|
+
*
|
|
89
|
+
* A suggestion chip is sent verbatim on click, so an unanswerable one is a
|
|
90
|
+
* button that does nothing. The filter is the matcher itself rather than a
|
|
91
|
+
* second list of blessed phrasings — a list would be one more thing to drift,
|
|
92
|
+
* and this cannot: if `demoPlanFromMessage` stops answering something, the chip
|
|
93
|
+
* for it stops being offered in the same commit.
|
|
94
|
+
*
|
|
95
|
+
* Returning fewer chips, or none, is the intended outcome. Three real ones beat
|
|
96
|
+
* six of which one works, and no chips at all leaves the keyless notice to say
|
|
97
|
+
* the thing that actually needs saying.
|
|
98
|
+
*/
|
|
99
|
+
export declare function keepDemoExecutable(suggestions: string[], slug: string, activeBlockId?: string, activeBlockType?: string): string[];
|
|
78
100
|
export {};
|
|
@@ -393,13 +393,18 @@ export function demoPlanFromMessage(message, slug, activeBlockId, activeBlockTyp
|
|
|
393
393
|
if (lower.includes("add testimonials")) {
|
|
394
394
|
return {
|
|
395
395
|
intent: "edit_plan",
|
|
396
|
-
summary_for_user: "Added a testimonials section
|
|
397
|
-
change_log: ["Inserted Testimonials block
|
|
396
|
+
summary_for_user: "Added a testimonials section.",
|
|
397
|
+
change_log: ["Inserted a Testimonials block at the end of the page."],
|
|
398
398
|
ops: [
|
|
399
399
|
{
|
|
400
400
|
op: "add_block",
|
|
401
401
|
pageSlug: slug,
|
|
402
|
-
afterBlockId
|
|
402
|
+
/* No `afterBlockId`. It used to name `b_hero_home`, a block id that
|
|
403
|
+
* exists on no page in the shipped demo — the home hero is
|
|
404
|
+
* `b_hero_1772138902220_copy` — so the anchor silently missed and the
|
|
405
|
+
* block landed whereever the fallback put it, while the summary
|
|
406
|
+
* promised "below the hero". Appending is the honest version of what
|
|
407
|
+
* it already did, and it cannot point at a block that is not there. */
|
|
403
408
|
block: {
|
|
404
409
|
id: `b_testimonials_${Date.now()}`,
|
|
405
410
|
type: "Testimonials",
|
|
@@ -627,3 +632,34 @@ export function withKeylessNotice(plan) {
|
|
|
627
632
|
return plan;
|
|
628
633
|
return { ...plan, summary_for_user: KEYLESS_PLANNER_NOTICE };
|
|
629
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* Drop the suggestions this planner could not act on.
|
|
637
|
+
*
|
|
638
|
+
* `postEditSuggestions` and `clarificationSuggestions` are written against the
|
|
639
|
+
* product — "Edit Banner", "Move FAQ", "Add author photos" — because with a key
|
|
640
|
+
* configured a model answers them, and it can. With no key the answer comes
|
|
641
|
+
* from `demoPlanFromMessage`, a short list of English substring matchers, and
|
|
642
|
+
* almost none of those phrasings is on it. A clean-room first run clicked six
|
|
643
|
+
* follow-up chips against a keyless scaffold; five produced zero operations and
|
|
644
|
+
* the same paragraph about adding a key.
|
|
645
|
+
*
|
|
646
|
+
* A suggestion chip is sent verbatim on click, so an unanswerable one is a
|
|
647
|
+
* button that does nothing. The filter is the matcher itself rather than a
|
|
648
|
+
* second list of blessed phrasings — a list would be one more thing to drift,
|
|
649
|
+
* and this cannot: if `demoPlanFromMessage` stops answering something, the chip
|
|
650
|
+
* for it stops being offered in the same commit.
|
|
651
|
+
*
|
|
652
|
+
* Returning fewer chips, or none, is the intended outcome. Three real ones beat
|
|
653
|
+
* six of which one works, and no chips at all leaves the keyless notice to say
|
|
654
|
+
* the thing that actually needs saying.
|
|
655
|
+
*/
|
|
656
|
+
export function keepDemoExecutable(suggestions, slug, activeBlockId, activeBlockType) {
|
|
657
|
+
return suggestions.filter((suggestion) => {
|
|
658
|
+
try {
|
|
659
|
+
return demoPlanFromMessage(suggestion, slug, activeBlockId, activeBlockType).ops.length > 0;
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
}
|
|
@@ -78,7 +78,7 @@ export declare function compileDeterministicPlan(args: {
|
|
|
78
78
|
locale?: string;
|
|
79
79
|
}): EditPlan | null;
|
|
80
80
|
export { extractAudienceTarget, extractAudienceTargets, titleCaseWords, addAudienceSuffix, audiencePatchForBlock, coercePatchForBlock, parseIndexedPath, inferSimpleFieldPatchFromMessage, isRewriteRequest, isTranslationRequest, shouldKeepRichTextTitleOnTranslate, inferFieldHintFromMessage, rewriteFromExisting, coercePatchForEditablePath, quotedText, buildListAppendPatch } from "./deterministic-planner-patches.ts";
|
|
81
|
-
export { editablePropsFromBlock, promptFromPropKey, userFacingPropNames, ORDINALS, humanizeArrayPath, childSuggestions, clarificationSuggestions, postEditSuggestions, demoPlanFromMessage, withKeylessNotice, KEYLESS_PLANNER_NOTICE, titleCaseSentence, pageMetaContractSummary, blockContractsSummary } from "./deterministic-planner-suggestions.ts";
|
|
81
|
+
export { editablePropsFromBlock, promptFromPropKey, userFacingPropNames, ORDINALS, humanizeArrayPath, childSuggestions, clarificationSuggestions, postEditSuggestions, demoPlanFromMessage, keepDemoExecutable, withKeylessNotice, KEYLESS_PLANNER_NOTICE, titleCaseSentence, pageMetaContractSummary, blockContractsSummary } from "./deterministic-planner-suggestions.ts";
|
|
82
82
|
export { nextAvailableSlug, createPageBlocks, buildCreatePagePlan, isPageRouteRenameRequest } from "./deterministic-planner-pages.ts";
|
|
83
83
|
export { readPathValue, resolveImageUrlForAltField, fetchImageAsBase64, resolveAttachmentsForLlm, selectedBlockSnapshot, arrayPropLengths, pageIntentSummary, plannerContextPack } from "./deterministic-planner-context.ts";
|
|
84
84
|
export type { ResolvedLlmAttachment } from "./deterministic-planner-context.ts";
|
|
@@ -1628,7 +1628,7 @@ export function compileDeterministicPlan(args) {
|
|
|
1628
1628
|
// Re-exports from split files (preserve public API)
|
|
1629
1629
|
// ---------------------------------------------------------------------------
|
|
1630
1630
|
export { extractAudienceTarget, extractAudienceTargets, titleCaseWords, addAudienceSuffix, audiencePatchForBlock, coercePatchForBlock, parseIndexedPath, inferSimpleFieldPatchFromMessage, isRewriteRequest, isTranslationRequest, shouldKeepRichTextTitleOnTranslate, inferFieldHintFromMessage, rewriteFromExisting, coercePatchForEditablePath, quotedText, buildListAppendPatch } from "./deterministic-planner-patches.js";
|
|
1631
|
-
export { editablePropsFromBlock, promptFromPropKey, userFacingPropNames, ORDINALS, humanizeArrayPath, childSuggestions, clarificationSuggestions, postEditSuggestions, demoPlanFromMessage, withKeylessNotice, KEYLESS_PLANNER_NOTICE, titleCaseSentence, pageMetaContractSummary, blockContractsSummary } from "./deterministic-planner-suggestions.js";
|
|
1631
|
+
export { editablePropsFromBlock, promptFromPropKey, userFacingPropNames, ORDINALS, humanizeArrayPath, childSuggestions, clarificationSuggestions, postEditSuggestions, demoPlanFromMessage, keepDemoExecutable, withKeylessNotice, KEYLESS_PLANNER_NOTICE, titleCaseSentence, pageMetaContractSummary, blockContractsSummary } from "./deterministic-planner-suggestions.js";
|
|
1632
1632
|
export { nextAvailableSlug, createPageBlocks, buildCreatePagePlan, isPageRouteRenameRequest } from "./deterministic-planner-pages.js";
|
|
1633
1633
|
export { readPathValue, resolveImageUrlForAltField, fetchImageAsBase64, resolveAttachmentsForLlm, selectedBlockSnapshot, arrayPropLengths, pageIntentSummary, plannerContextPack } from "./deterministic-planner-context.js";
|
|
1634
1634
|
export { inferAddedBlockTypeFromMessage, resolveBlockRef, ordinalToIndex, resolveByDescriptor, resolveReferencesFromMessage } from "./deterministic-planner-refs.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { generateVariationImageWithOpenAI, generateVariationImageWithGemini, recordImageGenDuration, estimatedImageGenMs } from "../../image/image-helpers.js";
|
|
1
|
+
import { generateVariationImageWithOpenAI, generateVariationImageWithGemini, recordImageGenDuration, estimatedImageGenMs, resolveImageProvider } from "../../image/image-helpers.js";
|
|
2
2
|
import { getPage } from "../../state/session-state.js";
|
|
3
3
|
const ASPECT_RATIO_TO_SIZE = {
|
|
4
4
|
landscape: "1536x1024",
|
|
@@ -170,7 +170,9 @@ export const imageGenerateHandler = async ({ input, context, signal }) => {
|
|
|
170
170
|
const pct = Math.min(Math.round(cur.pct + stageProgress * (next.pct - cur.pct)), 95);
|
|
171
171
|
onProgress({ percent: pct, stage: cur.label });
|
|
172
172
|
}, 500) : null;
|
|
173
|
-
|
|
173
|
+
/* Key-aware, not just env-aware: a deployment holding only
|
|
174
|
+
* GOOGLE_GENAI_API_KEY used to land on the OpenAI branch here and fail. */
|
|
175
|
+
const provider = resolveImageProvider();
|
|
174
176
|
const genStart = Date.now();
|
|
175
177
|
let result = null;
|
|
176
178
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/orchestrator-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./package.json": "./package.json",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"openai": "^4.87.1",
|
|
23
23
|
"sharp": "^0.35.4",
|
|
24
24
|
"zod": "^4.3.6",
|
|
25
|
-
"@avocadostudio-ai/shared": "^0.
|
|
26
|
-
"@avocadostudio-ai/migration-sdk": "^0.
|
|
25
|
+
"@avocadostudio-ai/shared": "^0.11.0",
|
|
26
|
+
"@avocadostudio-ai/migration-sdk": "^0.11.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@anthropic-ai/claude-agent-sdk": "^0.3.220",
|