Skip to content

Resume import fails with Anthropic: “compiled grammar is too large” #24

Closed

Description

@ruchindwd

What happened?

Provider: Anthropic, tested with claude-sonnet-5-5 and claude-haiku-4-5

What happens: Importing a resume in Profile fails with “Something went wrong while working on this.” The log shows AiError: The AI provider ran into a problem. Test connection works.

Also: RangeError: Invalid time zone specified: Etc/Unknown appears in the logs while TZ is unset; setting TZ fixes it.

What did you expect to happen?

Cause: The extracted_profile schema from extract-profile.ts (via zodOutputFormat) is rejected by the Messages API. Sending that schema directly in output_config.format, with a three-line resume, returns:
400 invalid_request_error: The compiled grammar is too large, which would cause performance issues. Simplify your tool schemas or reduce the number of strict tools.
(request_id: req_011CgUiKPa2umKFjM8jGbmpb)

How can we make it happen?

The schema has about 32 nullable fields. Anthropic’s structured outputs docs list a limit of 16 parameters with union types, and say larger schemas can fail to compile.
Possible fix: On this 400, retry without output_config.format, with the schema described in the prompt; the answer is already validated with zod. Or simplify the schema, for example by using empty strings instead of null.

Dossier version

1.9.2 (Docker image, ghcr.io/aientech/dossier:latest)

How do you run Dossier?

Docker Compose, as in the README

AI provider

None

Model

No response

Browser and system

No response

Server log


Anything else

No response

Before you send it

  • I searched the issues, and this hasn't been reported yet.
  • What I've written and pasted has no passwords, API keys or personal details in it.

Activity

  1. Jchinonso commented on Oct 29, 2026

    @Jchinonso

    Same issue here: Dossier 1.9.2 built from source with Node.js, Anthropic claude-sonnet-5. Profile import from a PDF failed with "The AI provider ran into a problem", and Test connection worked. No failed request showed up in the Console's Logs.

    I tried the first fix suggested above. If the request with output_config.format gets a 400, it retries once without the format and puts the schema in the system prompt with withJsonSchema. The answer still goes through the same zod validation. Import worked on the first try after that. The AI tests pass: yarn vitest run src/lib/ai, 154 passed.

    Patch to src/lib/ai/providers/anthropic.ts:

    diff --git a/src/lib/ai/providers/anthropic.ts b/src/lib/ai/providers/anthropic.ts
    index 8e45d13..105648b 100644
    --- a/src/lib/ai/providers/anthropic.ts
    +++ b/src/lib/ai/providers/anthropic.ts
    @@ -4,7 +4,7 @@ import Anthropic from "@anthropic-ai/sdk";
     import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
     
     import { AiError } from "../errors";
    -import { BaseProvider, type ChatAnswer, type ChatCall, type Effort } from "../provider";
    +import { BaseProvider, type ChatAnswer, type ChatCall, type Effort, withJsonSchema } from "../provider";
     import { keyGuardedFetch } from "./guarded-fetch";
     
     /** Offered when the model list can't be fetched, e.g. before a key is saved. */
    @@ -44,17 +44,29 @@ export class AnthropicProvider extends BaseProvider {
       protected async chat(call: ChatCall): Promise<ChatAnswer> {
         if (!this.client) throw AiError.config("Add your Anthropic API key in Settings → AI provider first.");
         const effort = call.effort && (await this.effortLevels(this.client, call.model, call.signal)).has(call.effort) ? call.effort : undefined;
    -    const outputConfig = { ...(call.json ? { format: zodOutputFormat(call.json.schema) } : {}), ...(effort ? { effort } : {}) };
    -    const message = await this.client.messages.create(
    -      {
    -        model: call.model,
    -        max_tokens: call.maxTokens + THINKING_TOKENS,
    -        system: call.system,
    -        messages: call.messages,
    -        ...(Object.keys(outputConfig).length > 0 ? { output_config: outputConfig } : {}),
    -      },
    -      { signal: call.signal },
    -    );
    +    const send = (structured: boolean) => {
    +      const outputConfig = { ...(structured && call.json ? { format: zodOutputFormat(call.json.schema) } : {}), ...(effort ? { effort } : {}) };
    +      return this.client!.messages.create(
    +        {
    +          model: call.model,
    +          max_tokens: call.maxTokens + THINKING_TOKENS,
    +          system: !structured && call.json ? withJsonSchema(call.system, call.json.schema) : call.system,
    +          messages: call.messages,
    +          ...(Object.keys(outputConfig).length > 0 ? { output_config: outputConfig } : {}),
    +        },
    +        { signal: call.signal },
    +      );
    +    };
    +    let message: Anthropic.Message;
    +    try {
    +      message = await send(true);
    +    } catch (error) {
    +      // Structured outputs refuse schemas with many nullable fields (the
    +      // profile import has 32) with a 400; the schema then goes in the prompt
    +      // instead, and the answer is still validated against it.
    +      if (!call.json || !(error instanceof Anthropic.BadRequestError)) throw error;
    +      message = await send(false);
    +    }
         if (message.stop_reason === "refusal") {
           throw new AiError("provider", "The model declined to answer this one. Try rewording it, or try again.", "refusal");
         }

    It retries on any BadRequestError when a schema was sent. You might prefer to match only the "compiled grammar is too large" message, so other 400s fail fast.

  2. added a commit that references this issue on Oct 29, 2026
  3. AienTech commented on Oct 29, 2026

    @AienTech
    Owner

    Thanks both! Both parts are fixed in 1.10.0:

    • Importing a resume: when Anthropic refuses the answer's shape as too complex, Dossier asks once more with that shape in the instructions, and remembers to for that model (from fix: put the schema in the prompt when Anthropic refuses it as too complex #16).
    • Etc/Unknown: TZ now defaults to UTC, and Dossier uses UTC whenever the server's time zone isn't one it knows. If you use compose.production.yml, take the new one.

    @Jchinonso, your #30 does the same as #16, so I'm closing it in favour of the release. Thanks for testing it by hand.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions