-
Notifications
You must be signed in to change notification settings - Fork 11
Merge dev into main: model-policy fallback and clear review error surfacing #107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
52c8513
feat(review): fall back gracefully when the org model policy blocks t…
factory-nizar 98bf871
fix(review): strip stale review/security progress text from the final…
factory-nizar 3a9aaeb
feat(review): fall back on invalid model ids and condense model-list …
factory-nizar 4028e0a
Merge pull request #106 from Factory-AI/nizar/model-policy-fallback
factory-nizar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| /** | ||
| * Matches the 403 errors returned by the Factory API when a request uses a | ||
| * model that the organization's model policy does not allow, including the | ||
| * explicit opt-in variant ("This model requires explicit organization | ||
| * opt-in by an admin."). | ||
| */ | ||
| const MODEL_POLICY_ERROR_PATTERNS = [ | ||
| /not available due to your organization['’]s security settings/i, | ||
| /requires explicit organization opt-in/i, | ||
| ]; | ||
|
|
||
| export function isModelPolicyError(text: string | undefined | null): boolean { | ||
| if (!text) { | ||
| return false; | ||
| } | ||
| return MODEL_POLICY_ERROR_PATTERNS.some((pattern) => pattern.test(text)); | ||
| } | ||
|
|
||
| /** | ||
| * Matches the fast client-side failure droid exec prints to stderr when the | ||
| * --model value is not a recognized model id. | ||
| */ | ||
| export function isInvalidModelError(text: string | undefined | null): boolean { | ||
| if (!text) { | ||
| return false; | ||
| } | ||
| return /Invalid model:/i.test(text); | ||
| } | ||
|
|
||
| /** | ||
| * An invalid --model value makes droid exec dump the full list of available | ||
| * models (twice, with one line per dump that is hundreds of characters | ||
| * wide). Condense that output down to the meaningful lines so it can be | ||
| * embedded in a PR comment without sideways scrolling. | ||
| */ | ||
| export function condenseInvalidModelError(text: string): string { | ||
| const kept: string[] = []; | ||
| for (const line of text.split("\n")) { | ||
| const trimmed = line.trim(); | ||
| if (trimmed === "") continue; | ||
| if (/^Available built-in models:$/i.test(trimmed)) continue; | ||
| if (/^No custom models configured/i.test(trimmed)) continue; | ||
| // Drop raw model-list dumps (long comma-separated lines) | ||
| if (trimmed.split(", ").length >= 5) continue; | ||
| if (kept[kept.length - 1] === trimmed) continue; | ||
| kept.push(trimmed); | ||
| } | ||
| const condensed = kept.join("\n"); | ||
| return condensed || text; | ||
| } | ||
|
|
||
| /** | ||
| * Remove `--model <value>` and `--reasoning-effort <value>` (including | ||
| * `--flag=value` forms) from an argv array so droid exec falls back to the | ||
| * organization's default model. | ||
| */ | ||
| export function stripModelArgs(args: string[]): string[] { | ||
| const stripped: string[] = []; | ||
| let skipNext = false; | ||
|
|
||
| for (const arg of args) { | ||
| if (skipNext) { | ||
| skipNext = false; | ||
| continue; | ||
| } | ||
| if (arg === "--model" || arg === "--reasoning-effort") { | ||
| skipNext = true; | ||
| continue; | ||
| } | ||
| if (arg.startsWith("--model=") || arg.startsWith("--reasoning-effort=")) { | ||
| continue; | ||
| } | ||
| stripped.push(arg); | ||
| } | ||
|
|
||
| return stripped; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| import { describe, expect, it } from "bun:test"; | ||
| import { | ||
| condenseInvalidModelError, | ||
| isInvalidModelError, | ||
| isModelPolicyError, | ||
| stripModelArgs, | ||
| } from "../src/utils/model-policy-error"; | ||
|
|
||
| describe("isModelPolicyError", () => { | ||
| it("matches the model policy 403 message", () => { | ||
| expect( | ||
| isModelPolicyError( | ||
| `403 {"detail":"This model is not available due to your organization's security settings.","status":403}`, | ||
| ), | ||
| ).toBe(true); | ||
| }); | ||
|
|
||
| it("matches with a curly apostrophe", () => { | ||
| expect( | ||
| isModelPolicyError( | ||
| "This model is not available due to your organization’s security settings.", | ||
| ), | ||
| ).toBe(true); | ||
| }); | ||
|
|
||
| it("matches the explicit opt-in 403 message", () => { | ||
| expect( | ||
| isModelPolicyError( | ||
| "403 This model requires explicit organization opt-in by an admin.", | ||
| ), | ||
| ).toBe(true); | ||
| }); | ||
|
|
||
| it("does not match unrelated errors", () => { | ||
| expect(isModelPolicyError("429 Too Many Requests")).toBe(false); | ||
| expect(isModelPolicyError(undefined)).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe("isInvalidModelError", () => { | ||
| it("matches the CLI invalid-model stderr output", () => { | ||
| expect(isInvalidModelError("Invalid model: gpt-image-1")).toBe(true); | ||
| }); | ||
|
|
||
| it("does not match unrelated output", () => { | ||
| expect(isInvalidModelError("500 Internal Server Error")).toBe(false); | ||
| expect(isInvalidModelError(undefined)).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe("condenseInvalidModelError", () => { | ||
| it("drops the model-list dump and dedupes repeated lines", () => { | ||
| const dump = [ | ||
| "claude-opus-5, claude-sonnet-5, gpt-5.4, gpt-5.2, kimi-k3, glm-5.2", | ||
| "", | ||
| "No custom models configured. Add them to ~/.factory/settings.json", | ||
| "Invalid model: gpt-image-1", | ||
| "", | ||
| "Available built-in models:", | ||
| " auto, claude-opus-5, claude-sonnet-5, gpt-5.4, gpt-5.2, kimi-k3", | ||
| "", | ||
| "No custom models configured. Add them to ~/.factory/settings.json", | ||
| "Invalid model: gpt-image-1", | ||
| ].join("\n"); | ||
|
|
||
| expect(condenseInvalidModelError(dump)).toBe("Invalid model: gpt-image-1"); | ||
| }); | ||
|
|
||
| it("returns the original text when nothing would remain", () => { | ||
| const listOnly = "a, b, c, d, e, f"; | ||
| expect(condenseInvalidModelError(listOnly)).toBe(listOnly); | ||
| }); | ||
| }); | ||
|
|
||
| describe("stripModelArgs", () => { | ||
| it("removes --model and its value", () => { | ||
| expect( | ||
| stripModelArgs(["exec", "--model", "gpt-5.2", "-f", "prompt.txt"]), | ||
| ).toEqual(["exec", "-f", "prompt.txt"]); | ||
| }); | ||
|
|
||
| it("removes --reasoning-effort and its value", () => { | ||
| expect( | ||
| stripModelArgs(["exec", "--reasoning-effort", "high", "-f", "p.txt"]), | ||
| ).toEqual(["exec", "-f", "p.txt"]); | ||
| }); | ||
|
|
||
| it("removes --flag=value forms", () => { | ||
| expect( | ||
| stripModelArgs(["exec", "--model=gpt-5.2", "--reasoning-effort=high"]), | ||
| ).toEqual(["exec"]); | ||
| }); | ||
|
|
||
| it("removes both flags while preserving other args", () => { | ||
| expect( | ||
| stripModelArgs([ | ||
| "exec", | ||
| "--output-format", | ||
| "stream-json", | ||
| "--model", | ||
| "kimi-k2.6", | ||
| "--reasoning-effort", | ||
| "high", | ||
| "--tag", | ||
| "code-review", | ||
| ]), | ||
| ).toEqual([ | ||
| "exec", | ||
| "--output-format", | ||
| "stream-json", | ||
| "--tag", | ||
| "code-review", | ||
| ]); | ||
| }); | ||
|
|
||
| it("returns args unchanged when no model flags are present", () => { | ||
| const args = ["exec", "--output-format", "stream-json", "-f", "p.txt"]; | ||
| expect(stripModelArgs(args)).toEqual(args); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P1] Reset lastResultEvent between retry attempts
lastResultEventis not cleared per attempt, so a later retry that exits before emitting atype: "result"event can cause stale result data from a previous attempt to drive the retry decision and the finalerror_message. Reset it at the start ofrunDroidOnceso each attempt’s diagnostics reflect only that attempt.