[1.0.0] - 2026-10-16
ContextGem 1.0 adds decision models next to LLMs. A decision model answers typed questions about a text (yes/no, pick one option, rate on a scale) with the probability of each possible answer, and generates no text. It extracts aspects, and Boolean, label and rating concepts, on its own, in a model group together with LLMs, or in a fallback chain, so that each aspect and concept can go to the model that suits it. The names in the API that are not specific to LLMs now refer to models in general, which is one of the breaking changes of this release: see the migration guide before you upgrade.
Breaking changes
See the migration guide for updating the code and the serialized data.
- Renamed the classes, parameters, and attributes that are not specific to LLMs, so that their names refer to models in general.
DocumentLLMandLLMToolLoopLimitErrorkeep their names. The old names are removed, and using them raises an error. See the migration guide for updating the code and the serialized data.DocumentLLMGroupis renamed toDocumentModelGroup, and itsllmsparameter and attribute tomodels.- The
llm_roleparameter and attribute of aspects and concepts is renamed tomodel_role, as is thellm_roleargument ofget_usage(),get_cost(), andreset_usage_and_cost()ofDocumentModelGroup. - The
llm_rolesproperty of documents, pipelines, and aspects is renamed tomodel_roles. LLMPricingis renamed toModelPricing.LLMAPIErroris renamed toModelAPIError, andLLMExtractionErrortoModelExtractionError.
- Serialized data uses the new names: the
model_rolekey of aspects and concepts, themodelskey and theDocumentModelGroupclass name of model groups, theModelPricingclass name of pricing details, and the class names of the usage and cost containers stored in serialized LLMs (always empty, as the usage and cost statistics are not serialized). Aspects, concepts, LLMs, and model groups serialized by earlier versions, as well as the documents and pipelines that have aspects or concepts, can no longer be deserialized as is. Update the serialized data as described in the migration guide. - Model roles are now free-form labels. The
roleparameter ofDocumentLLMand themodel_roleparameter of aspects and concepts accept any label of up to 64 printable characters (the surrounding whitespace is removed; line breaks, tabs, and other control or invisible characters are not allowed) and default toNone(no role), instead of one of the six predefined role names ("extractor_text","reasoner_text","extractor_vision","reasoner_vision","extractor_multimodal","reasoner_multimodal"; for aspects, one of the two*_textnames), with"extractor_text"as the default. A role only routes aspects and concepts to the models of aDocumentModelGroup: an aspect or concept is extracted by the model with theroleequal to itsmodel_role, and an aspect or concept withoutmodel_roleis extracted by the model withoutrole. A role no longer implies the capabilities of the model or the content it can process, so the same model no longer needs to be added to a group several times, with different roles, to process both text and images. Each model in a group must still have a unique role, i.e. only one model in a group can have no role. The predefined role names have no special meaning anymore: if still used, they are treated as any other role labels, and no longer determine the content a concept is extracted from. See the migration guide for updating the code and the serialized data. - A
DocumentLLMused on its own (not in a group) now extracts all the aspects and concepts it is given, regardless of theirmodel_role. Previously, it skipped the aspects and concepts with amodel_roledifferent from itsrole, with a warning. To extract only some of the aspects or concepts with a single LLM, use thefrom_aspects/from_conceptsparameters of the extraction methods. - A
DocumentModelGroupnow raisesValueErrorif an aspect or concept to be extracted has amodel_rolethat no model in the group has. This is checked before sending any requests for that extraction method, including for the nested aspects and concepts that the method processes. Previously, such aspects and concepts were skipped: with a warning inextract_all(), and silently in the other extraction methods. Set themodel_roleof each aspect and concept to theroleof a model in the group, or leave it unset for the model without a role. - The content a concept is extracted from is now determined by the
modalityof the concept instead of the role of the LLM. Concepts with different modalities can be extracted by the same LLM, in separate calls. Setmodalityon the concepts to be extracted from images, as described in the migration guide. - The
model_roleargument ofget_usage(),get_cost(), andreset_usage_and_cost()ofDocumentModelGroupis now validated in the same way as the roles assigned to models: the surrounding whitespace is removed, and an invalid role, e.g. an empty string, raisesValueError. Previously, the argument was compared as passed, an empty string selected all LLMs, and the model without a role was selected with"extractor_text". The model without a role cannot be selected withmodel_role, asNoneselects all models: filter the returned entries byrole is None, or call the method on that model. - The
model_rolesproperty of documents, pipelines, and aspects now also includes the roles assigned to sub-aspects and their concepts, and containsNoneif there are aspects or concepts without a role, where it contained"extractor_text"before. Code that expects a set of strings, e.g. sorts the roles, has to handleNone. - The
list_rolesproperty ofDocumentLLMandDocumentModelGroup, and theroleof the entries returned byget_usage()andget_cost(), are nowNonefor a model without a role. Previously, they were"extractor_text"for an LLM created without a role. Check forNonewhere code compares these roles with"extractor_text". - Serialized
DocumentLLMdata now stores fallback models under thefallback_modelskey instead of thefallback_llmkey. An LLM or model group serialized by earlier versions has thefallback_llmkey in each LLM, even if no fallback LLM was assigned, and can no longer be deserialized as is. Update the serialized data as described in the migration guide, or create the LLMs anew. - A tool for chat is now defined in a single way: as a function decorated with
@register_tool, which is passed totoolsofDocumentLLM.toolsno longer accepts tool definitions as dictionaries in the OpenAI-compatible schema, andllm.toolsreturns the functions instead of the tool schemas. The schema of a tool is always generated from the function. To update the tools defined with dictionaries, see the migration guide. @register_toolnow raises an error for a function that has no docstring, or that accepts*argsor**kwargs. A parameter without a type hint is now reported when the function is decorated, rather than when the LLM is created. Previously, such a function was accepted: a tool without a docstring was sent to the LLM without a description, and*argsand**kwargswere ignored. Give each tool function a docstring, and parameters with names and type hints.- Tools with the same name are now rejected when they are set on an LLM. Previously, the last of them silently replaced the others. Give each tool of an LLM a name of its own.
- A fallback LLM must now have the same
system_messageas the primary LLM, which is checked when the fallback LLMs are set and when the system message of the primary LLM is changed. If the primary LLM is created withsystem_message, e.g. with an empty string to disable it for a chat, pass the same value to each of its fallback LLMs. Previously, a fallback LLM could have another system message: a fallback LLM created withoutsystem_messageused the default system message, which is written for extraction, also in a chat for which the primary LLM had another system message or none. In a chat with aChatSession, a fallback LLM was sent the system message of the session instead of its own, or, if the session had none, added its own system message to the session, which was then sent with the later requests of the primary LLM as well. See the migration guide. - In chat, an empty response of an LLM is now handled as a failure of that LLM: the next fallback LLM is tried, and
ModelAPIErroris raised if there is no fallback LLM left to try. Previously, a response without a text was passed to the fallback LLM or raisedRuntimeError, while a response with an empty text was returned as the answer, or failed with a validation error when a chat session was used. Where code handled an empty answer, catchModelAPIErrorinstead. - A function used as a tool in chat must return a non-empty string: for an empty string,
ValueErroris now raised fromchat(). Previously, an empty string failed the request with a validation error reported asModelAPIError. Return a text from a tool that has nothing to report, e.g."No results". - The output tokens of a response are now limited by a single parameter of
DocumentLLM,max_tokens, which applies to all models and is not set by default: no limit is sent with the requests, and the default of the model applies, which for most providers is the maximum output of the model. Previously,max_tokens(4096 by default) applied to the models without reasoning support, andmax_completion_tokens(16000 by default, removed in this version, see below) to reasoning (CoT-capable) models, while the other of the two parameters was ignored. Amax_tokensvalue set for a reasoning model, which was ignored before, now limits the output of the model, including its reasoning tokens.max_tokensis checked against the maximum output of the model only if it is set, so the default no longer raisesValueErrorfor the models with a maximum output below the previous default limits. See the migration guide. - A response cut off at the max output tokens is no longer requested again during extraction, as the same request would be cut off in the same way: the next fallback model is tried, and
ModelAPIErroris raised if there is no fallback model left to try, with a message telling that the response was cut off and how to get a complete one (or a warning is logged, ifraise_exception_on_extraction_error=Falseis passed). Previously, such a response was handled as invalid data: it was requested again up tomax_retries_invalid_datatimes, and the extraction failed withModelExtractionErrorreporting invalid JSON. To get a complete response, increasemax_tokensor leave it unset, as the message says, or catchModelAPIErrorwhereModelExtractionErrorwas caught for it. - The
temperatureofDocumentLLMis no longer set by default: the parameter is not sent, and the default of the model applies. Previously, it defaulted to 0.3, which was sent to the models without reasoning support only. To keep the previous behavior for such a model, e.g. a local model, settemperature=0.3. See the migration guide. DocumentModelGroupis moved from thecontextgem.public.llmsmodule to the newcontextgem.public.modelsmodule, as a model group is not specific to LLMs. It can no longer be imported fromcontextgem.public.llms. Importing it fromcontextgemworks as before. Serialized data is not affected, as it stores the class names without the module paths. See the migration guide.- Removed the
fallback_llmparameter and attribute ofDocumentLLM. Passingfallback_llmtoDocumentLLM, or reading or assigning the attribute, now raises an error. Usefallback_modelsinstead, e.g.fallback_models=[fallback_llm]. - Removed
DocumentPipeline, deprecated since v0.14.1. UseExtractionPipelineinstead, which has the same parameters and methods. A pipeline serialized asDocumentPipelinecan no longer be deserialized as is: replace the class name in the serialized data, as described in the migration guide. - Removed
RatingScale, deprecated since v0.10.0. Pass the rating scale of aRatingConceptas a tuple of(start, end)integers instead, e.g.rating_scale=(1, 5). Documents, pipelines, aspects, and concepts serialized with a rating scale defined as aRatingScaleobject can no longer be deserialized as is: update the serialized data as described in the migration guide. - Removed
DocxConverter, deprecated since v0.22.0, together with the DOCX converter exceptions (DocxConverterErrorand its subclasses). Use a dedicated document conversion library (e.g. Docling, MarkItDown) to convert files to text, then pass the result toDocument(raw_text=...). - Removed the Markdown text that
DocxConverterattached to documents and paragraphs and that was sent to the LLM instead of the plain text. Text is now always extracted fromraw_text. Documents converted from DOCX and serialized by earlier versions need no update for this removal: when they are deserialized, their Markdown text is ignored, and it is not kept when they are serialized again, while the additional context of their paragraphs is preserved. - Removed the
max_completion_tokensparameter and attribute ofDocumentLLM. Usemax_tokensinstead, which now applies to all models (see above). Passingmax_completion_tokenstoDocumentLLM, or reading or assigning the attribute, now raises an error. An LLM or model group serialized by earlier versions has themax_completion_tokenskey in each LLM, and can no longer be deserialized as is: update the serialized data as described in the migration guide. - Removed support for Python 3.10, which has reached its end of life. ContextGem now requires Python 3.11–3.14. The
onnxruntime<1.24.0constraint, which applied to Python 3.10 only, is removed as well. On Python 3.10, package installers such aspipanduvdo not install this version, and resolve to the latest earlier one: upgrade to Python 3.11 or later to use it. to_disk()andfrom_disk()now raise the error of a file that cannot be written or read as it is, e.g.FileNotFoundErrororPermissionError, andfrom_disk()raises the error of what the file holds asfrom_json()raises it, e.g.json.JSONDecodeErroror pydantic'sValidationError(subclasses ofValueError), orTypeErrorfor the data of another class, with a note that names the file. Previously, each of these errors was raised asRuntimeError, with the error as its cause, so the calling code could not handle e.g. a missing file by its type. Catch these errors whereRuntimeErrorwas caught. See the migration guide.assign_pipeline()now raisesValueErrorfor a document that already has aspects or concepts, unlessoverwrite_existing=Trueis passed, as for the other invalid arguments. Previously, it raisedRuntimeError.
Added
- Added
DocumentDecisionModel, a class for decision models, next toDocumentLLM. A decision model takes a text and typed questions (yes/no, pick one option, rate on a scale) and returns the probabilities of the answers; it generates no text. ADocumentDecisionModelextractsBooleanConcept,LabelConcept, andRatingConcept, from a document or from an aspect: the questions of the concepts extracted in one call are sent in one request, with the text of the document or of the paragraphs of the aspect. It also extracts aspects, including sub-aspects: each paragraph is sent in a request of its own, with one yes/no question per aspect, and becomes an extracted item of each aspect that it belongs to with a probability of the decision threshold (0.5 by default) or more. For an aspect withreference_depth="sentences", the sentences of the paragraphs that belong to the aspect are then asked about in the same way, and the sentences that belong to the aspect become its extracted items (see below). It has the extraction methods ofDocumentLLMand nochat(), and can be used on its own, in aDocumentModelGroup, and in fallback chains together with LLMs (see "Changed"). Requests are sent through the Decisions API oflitellm, which serves OpenAI, TypeSafe, Microsoft Foundry (asazure_ai/<deployment name>, with the endpoint of the Foundry resource asapi_base), Perplexity, OpenRouter, Cloudflare, and local or self-hosted servers: such as Ollama and LM Studio under thestrands_decider/prefix, and vLLM under thehosted_vllm/prefix, with the address of the server asapi_base.ValueErroris raised before any requests are sent for what a decision model cannot extract: concepts with open values (StringConcept,NumericalConcept,DateConcept,JsonObjectConcept), concepts with the"image"or"multimodal"modality, justifications, references for concepts, and concepts with more labels or rating levels thanlitellmaccepts in a question. A model of a provider thatlitellmserves no decision models of is rejected when the decision model is created. Failed requests are retried as for LLMs. A response that cannot be used, e.g. with a refused answer to a question of a concept, is not requested again: the next fallback model is tried, andModelExtractionErroris raised if there is no fallback model left to try. In aspect extraction, a paragraph or a sentence that the model refuses to answer on is not included in the aspect, and a warning logged names the aspect and what was refused: the call fails only if the model refuses to answer on every paragraph for an aspect. Usage and cost are tracked as for LLMs.auto_pricingworks for the models whose prices of decision requestsgenai-priceshas (e.g. the TypeSafe, Perplexity and Cloudflare models, andopenai/gpt-6-luna, whose prices of decision requestsgenai-priceshas apart from those of its chat requests), and is rejected for a decision model for whichgenai-priceshas the prices of a text-generating model, i.e. a price of output tokens (e.g.openrouter/openai/gpt-6-luna, for which the prices found under its provider are the chat prices), and for self-hosted ones (strands_decider/): setpricing_detailsto get their costs calculated. Set it also for a model deployed in Microsoft Foundry, whichgenai-priceshas no prices for. See the documentation. - Added
decision_threshold, the lowest probability of "yes" with which a decision model takes a yes/no question as answered "yes": a paragraph or a sentence belongs to an aspect, aBooleanConceptisTrue, a label of a multi-labelLabelConceptis selected. It is a parameter ofDocumentDecisionModel(0.5 by default), which applies to everything that the model extracts, and ofAspect,BooleanConceptandLabelConcept(not set by default), where it applies instead of the threshold of the decision model. It must be above 0 and not above 1. It does not apply to single-label and rating concepts, for which the most probable option is taken, and cannot be set for aLabelConceptwithclassification_type="multi_class". A sub-aspect does not get the threshold of its aspect. It does not change theprobabilitiesof the extracted items, and is ignored when an LLM extracts the aspect or the concept. The serialized data of aspects,BooleanConceptandLabelConceptnow includes thedecision_thresholdkey (nullif not set); data serialized by earlier versions has no such key, which needs no update. max_paragraphs_to_analyze_per_callapplies to the concepts that aDocumentDecisionModelextracts. When it is set and the text has more paragraphs, the paragraphs are split into consecutive parts of at most that many paragraphs, as for an LLM, and one request is sent for each part, so that a long document can be used with a model that has a small context, e.g. a local one. Each concept gets one extracted item for each part, in the order of the parts, each with its ownprobabilities: the answers are not merged across the parts, i.e. combining them is up to the calling code, and a multi-labelLabelConceptgets no item for a part in which no label reaches the decision threshold. Withuse_concurrency=True, the requests for the parts are sent concurrently, at the rate of theasync_limiterof the model. Without the parameter, the text is sent as a whole in one request. For aspects, the parameter changes nothing, as each paragraph is sent in a request of its own.- A concept with
singular_occurrence=Truecannot be extracted by aDocumentDecisionModelin parts: withmax_paragraphs_to_analyze_per_callset,ValueErroris raised before any requests are sent, also when the decision model is a fallback of the model that the concept is routed to. A decision model answers on every part of a text, so it cannot stop at the first part that gives an item, as an LLM does. Withoutmax_paragraphs_to_analyze_per_call, such a concept is extracted, with one item at most. - When a
DocumentDecisionModelextracts aspects, including sub-aspects, a paragraph that hasadditional_contextis sent with it, as an LLM is given it: the text of the request has the paragraph and its additional context, each with a label, and the question says that the paragraph is what is to be judged. A paragraph without additional context is sent as its text alone. Concepts are not affected. - A
DocumentDecisionModelextracts aspects withreference_depth="sentences", including sub-aspects, in two stages. First, each paragraph is sent in a request of its own, as forreference_depth="paragraphs". Then, each sentence of each paragraph that belongs to the aspect is sent in a request of its own, with the sentence, the paragraph that it is from as its context, and theadditional_contextof the sentence and of the paragraph, if set. A sentence with a probability of the decision threshold or more becomes an extracted item, with the sentence inreference_sentences, its paragraph inreference_paragraphs, and theprobabilitiesof the sentence: the same threshold applies at both stages. A paragraph that belongs to the aspect while none of its sentences does gives no items. The number of requests is the number of paragraphs plus the number of sentences in the paragraphs that belong to the aspect. - Added the
probabilitiesattribute to extracted items: a dictionary that maps each possible answer to its probability, as returned by a decision model. The keys are the answers as they appear as item values:TrueandFalsefor an item of aBooleanConceptand for an item of an aspect (whether the paragraph, or the sentence for an aspect withreference_depth="sentences", belongs to the aspect), the labels for an item of aLabelConcept, and the integers of the rating scale for an item of aRatingConcept. For a multi-labelLabelConcept, each label has the probability of its own yes/no question, so the probabilities do not sum to 1.probabilitiesisNonefor every item extracted by an LLM. Two items that differ only in their probabilities are not equal. - Added the
modalityparameter to concepts, which sets the document content a concept is extracted from:"text"(default),"image", or"multimodal"(text and/or images, whichever the document has)."image"and"multimodal"are supported for document-level concepts only. Previously, the content was determined by the role of the LLM (see "Breaking changes"). - Added the
fallback_modelsparameter toDocumentLLM, which accepts a list of fallback models and replaces thefallback_llmparameter, which accepted a single one (see "Breaking changes"). Fallback LLMs are tried in the given order until one of them succeeds, for both extraction and chat, e.g. to fall back across several providers during an outage. Each fallback LLM must haveis_fallback=Trueand the same role, output language, system message, and tool configuration as the primary LLM, must differ in configuration from the primary LLM and from the other fallback LLMs, and cannot have fallback models of its own. Usage and cost are tracked for the primary LLM and each of its fallback models. - Added tracking of cached input tokens. The usage returned by
get_usage()now includescache_readandcache_write: the number of input tokens that the LLM provider reports as read from, and written to, its prompt cache. Both are a part of theinputtoken count, and are zero if the provider does not report such tokens. - Added the optional
cache_read_per_1m_tokensandcache_write_per_1m_tokensparameters toModelPricing(previouslyLLMPricing, see "Changed"), which set the prices of the input tokens read from and written to the prompt cache of the LLM provider. If a cache price is not set, such tokens are priced as regular input tokens, as before. SerializedModelPricingdata now includes these keys. - Added support for
Annotatedtype hints in the functions used as tools in chat. A string inAnnotated, or pydantic'sField(description=...), describes a parameter and takes precedence over the description in the docstring. The constraints set withField(gt,ge,lt,le,multiple_offor numbers;min_length,max_lengthfor strings and collections;patternfor strings) are added to the tool schema sent to the LLM, and are enforced when the LLM calls the tool: for an invalid value, the tool is not called, and the LLM receives the validation error.Annotatedis also supported for the items of collections and the fields of aTypedDict. - Added
ModelAPIError,ModelExtractionErrorandLLMToolLoopLimitErrorto the public API: the exceptions can now be imported fromcontextgem, and are documented in the API reference. Previously, they could be imported only from the internalcontextgem.internal.exceptionsmodule, which still works. - Added
ContextGemError, the base class ofModelAPIError,ModelExtractionErrorandLLMToolLoopLimitError, to the public API:except ContextGemErrorcatches any of them. It can be imported fromcontextgem. An error in the calling code, e.g. an invalid argument, is still raised asValueErrororTypeError. - Added
llms.txtat the root of the documentation site: the index of the documentation for LLMs and coding agents, with a one-sentence description of each page, and a Markdown copy of each page next to its HTML one. It takes the place of the raw documentation filedocs/source/llms.txt, which was removed in 0.22.0. - In chat, the thinking blocks that an LLM returns with its tool calls and answers, e.g. those of Claude when
reasoning_effortis set, are now sent back to it unchanged: in the next tool rounds of the same chat call and, in a chat session, in the next turns. The LLM thus keeps its reasoning across tool rounds and turns, as Anthropic recommends. A chat session keeps the thinking blocks in its history, and sends them back only to the same model with the same tools and system message, as a provider may reject them otherwise: they are left out when a session is continued by another LLM, or after the tools of the LLM are changed. Extraction is not affected. - Added
ContextGemWarning, a subclass ofUserWarning, as the category of the Python warnings that ContextGem issues, which are about something in the calling code that will not have the expected effect, e.g. tools set on an LLM used for extraction. It can be imported fromcontextgem. Filter the warnings of ContextGem by it, e.g.warnings.filterwarnings("ignore", category=ContextGemWarning), or make them errors in the tests of the calling code: such a warning is attributed to the line of the calling code that caused it, so a filter by the module of ContextGem does not match it.
Changed
- Vision and reasoning capabilities of a model are no longer checked against the role of the LLM when the LLM is created, i.e. the related warnings are no longer issued. Instead,
ValueErroris raised before any requests are sent, if the LLM that is to extract concepts from images, or any of its fallback LLMs, is not detected as vision-capable (unless_supports_vision=Trueis set on the LLM). The same applies to the fallback LLMs when images are passed tochat(). Previously, such error was raised only when the images were sent to the model, i.e. for a fallback LLM only after the primary LLM failed. As before, the reasoning-specific prompt instructions are used based on the detected reasoning support of the model. The warning about theollama_chat/prefix not supporting image inputs is now issued when images are sent to such a model, instead of when the LLM is created. - Serialized concept data now includes the
modalitykey. Documents and pipelines serialized by earlier versions store the predefined role names and no modality, and need to be updated as described in the migration guide. - Failed LLM API requests are now retried by ContextGem instead of litellm, with
num_retries_failed_requestkeeping its meaning. Only transient errors (request timeout, rate limit, server or connection error) are retried; non-transient errors (e.g. invalid request, failed authentication, exceeded context window) fail right away or are passed to a fallback model, instead of being retried as before. Retries are performed with exponential backoff and jitter (about 1, 2, 4, 8 seconds, capped at 10 seconds, each randomly shortened by up to a half); previously, only rate limit errors were retried with a backoff, without jitter, and other errors were retried immediately. ModelAPIError.retry_count, as well as the corresponding warning messages, now report the number of retries actually attempted for the failed request, rather than the configured number of retries.- The warning issued when the pricing data of a model is not found with
auto_pricing=Truenow also suggests settingauto_pricing_refresh=True, unless it is already set, as the pricing data bundled withgenai-pricesmay not include a recently released model. - Upgraded pinned dependency versions:
litellm==1.105.1,genai-prices==0.1.13. Versions remain pinned to maintain stability and avoid occasional breaking changes and API inconsistencies observed in previous unpinned releases.openairemains pinned to2.54.0, the latest version supported bylitellm. As of this version,litellminstallsboto3as a required dependency. - ContextGem can now be installed with
pillow12 andpython-ulid4: the supported ranges arepillow>=11.3.0,<13.0.0andpython-ulid>=3.0.0,<5.0.0. - Functions decorated with
@register_toolcan now be assigned to thetoolsof an existingDocumentLLM(llm.tools = [my_tool]), as a list or a tuple. Previously, functions were accepted only when the LLM was created. - A prompt that is found to exceed the context window of an LLM before the request is sent is now handled as any other non-transient failure of that LLM: the next fallback LLM, which may have a larger context window, is tried, and
ValueErroris raised only if there is no fallback LLM left to try. Previously,ValueErrorwas raised right away, and the fallback LLMs were not tried. This applies to both extraction and chat. - In chat, the request parameters of the fallback LLMs are now checked before any request is sent:
ValueErroris raised ifparallel_tool_callsis set for a fallback LLM that is not detected as supporting parallel tool calls, or if the max output tokens of a fallback LLM exceed the limit of its model. Previously, this was checked only when the fallback LLM was to be used, i.e. after the primary LLM failed. - A fallback LLM can now have the same tools as the primary LLM in a different order. Previously, it was rejected as having a different tool configuration.
- In chat, a response cut off at the max output tokens is now returned with a warning logged. If such a response has no text, the
ModelAPIErrorraised for the empty response tells that the response was cut off. Previously, a cut-off response was returned without a warning. temperatureandtop_pare now sent to any model if they are set, including reasoning (CoT-capable) models, for which they were ignored before. Whether a value is supported for the model is left tolitellmand the LLM provider: iflitellmreports a parameter as not supported for the model (e.g. a temperature for OpenAI's GPT-5 and GPT-6 models, which accept only their default one), the request is sent without it. The message logged in this case is now a warning instead of an error.- The default
timeoutofDocumentLLMis raised from 120 to 600 seconds. Responses are not streamed, so a response has to be received in full within the timeout, and a long response of a reasoning model may take more than 120 seconds. A request that times out is retried as a transient error, so a request to an unresponsive endpoint now takes longer to fail, unless a lowertimeoutis set. - The default
async_limiterofDocumentLLM, which paces the requests sent withuse_concurrency=True, now allows 10 requests per 10 seconds instead of 3. Set the limiter explicitly if this exceeds the rate limits of your LLM provider account. - The
modelsof aDocumentModelGroupand thefallback_modelsof aDocumentLLMnow accept decision models (DocumentDecisionModel) as well as LLMs. In a group, aspects and concepts are routed to a decision model by its role in the same way as to an LLM, and theoutput_languageof the group applies to its LLMs only. An LLM can have a decision model as a fallback only if the decision model can extract everything that is given or routed to the LLM, which is checked when an extraction method is called, before any requests are sent. A decision model can always have an LLM as a fallback. The requirement that a fallback LLM has the same output language, system message, and tool configuration as the primary LLM applies between two LLMs. Inchat(), a decision model among the fallback models of an LLM is skipped. - Serialized data of extracted items now includes the
probabilitieskey: a list of[answer, probability]pairs, ornullfor an item without probabilities. Data serialized by earlier versions has no such key, which needs no update: its items are loaded withprobabilities=None. Data serialized by this version cannot be loaded by earlier versions. - The parameters that
DocumentLLMhas in common with other kinds of models (model,api_key,api_base,role,num_retries_failed_request,timeout, the pricing and the fallback parameters) now come first in therepr()of an LLM and in the key order of its serialized data. Serialized data is loaded regardless of the order of its keys. - A fallback model is now compared with the primary model, and with the other fallback models, by its class as well as by its configuration parameters: an instance of a subclass of
DocumentLLMwith the same parameters as the primary LLM is no longer rejected as its exact copy. - Creating a
DocumentLLMis now much faster, as the built-in prompt templates are compiled once per process and shared by the LLMs: about 1 ms instead of about 130 ms on the machine it was measured on, and about 2 ms instead of about 260 ms with a fallback LLM. Previously, the templates were compiled for each LLM that was created, and the template of the default system message was compiled again for each change of a parameter of an LLM and for eachchat()call. The first LLM created in a process still compiles the templates. The prompts sent to the LLMs are unchanged. - A call whose content is sent to an LLM in parts (
max_paragraphs_to_analyze_per_callormax_images_to_analyze_per_call) is now continued from the part that the LLM returned invalid data for when the call is made again: the parts that were answered with valid data are not sent again, and what was extracted from them is kept. Previously, each retry sent all the parts of the call again, from the first one, so that e.g. invalid data for the last of ten parts cost ten more requests. The retries are still those of the call, not of each part, and a fallback LLM still extracts from all the parts. - The documentation is now at contextgem.shcherbak.ai: the links of the package, the README and the package description lead there, and the links to
contextgem.dev, including those of earlier versions, lead to the same pages there. - What happens while ContextGem runs is now reported in the log only, and what in the calling code will not have the expected effect as a Python warning only (
ContextGemWarning, see "Added"). The failure of an extraction withraise_exception_on_extraction_error=False, a chat response cut off at the max output tokens and the answers that a decision model refuses to give in aspect extraction are logged at theWARNINGlevel, and are no longer issued as Python warnings as well: each occurrence is logged, while Python shows a warning with the same message only once for a line of code, andCONTEXTGEM_LOGGER_LEVELcontrols them as any other log message. Previously, such a message was both logged and issued as a Python warning, i.e. it was shown twice, and was shown also with the logger disabled. A response cut off at the max output tokens in extraction, which was logged as an error and issued as a warning, is now logged as an error and, without a fallback model left to try, as a warning that the call failed, as other failed calls are. Code that checked these messages withpytest.warns()or a warning filter is to read the log instead. - The request params of the fallback LLMs, such as
max_tokens, are now checked before any request of an extraction is sent, as before a chat: a fallback LLM withmax_tokensabove the max output of its model raisesValueErrorbefore the requests to the primary model are sent. Previously, the params of a fallback LLM were checked only when the fallback LLM was tried, i.e. after the requests to the primary model, and the error ended the extraction there. Noneassigned tosystem_messageof aDocumentLLMnow sets the default system message, asNonepassed when the LLM is created does. Previously, the system message stayedNone, and no system message was sent. To send no system message, set an empty string, as before.
Removed
- Removed the
lxmldependency, which was used only by the DOCX converter. - Removed the special handling of system messages for the retired OpenAI
o1-previewando1-minimodels, for which the system message was left out of the request, with a warning. The system message is now sent to any model. - Removed the warning issued when
parallel_tool_calls=Truewas set for a model with theopenai/gpt-5orazure/gpt-5name prefix. The warning was hardcoded for the first GPT-5 models, and was also issued for the later models with the same prefix. - Removed the
typing-extensionsdependency, which was used only for the typing features that are in the standard library as of Python 3.11. - Removed the Colab notebooks generated from the usage examples (
dev/notebooks/), together with the "Open in Colab" links in the README and in the documentation. The examples are shown in full on the documentation pages, and their sources are indev/usage_examples/. Links to the notebooks in themainbranch no longer work once this version is released. - Removed the
tethereddependency, together with the network egress scopes around the download of SaT models and around the refresh of the pricing data ofgenai-prices. Neither scope applied to the requests it was meant for:genai-pricesrefreshes its data in a thread of its own, which a scope does not follow, andhuggingface_hubdownloads the model files throughhf_xet, outside Python's sockets. ContextGem connects to the same hosts as before: if your application or network restricts outbound connections, allow Hugging Face (huggingface.co,hf.coand their subdomains) for the SaT models, andraw.githubusercontent.comforauto_pricing_refresh=True. If your code importstethered, declare it as a dependency of your own.
Fixed
- A
DocumentModelGroupnow also checks that its models have unique roles when extraction starts, and raisesValueErrorbefore sending any requests if they do not. Previously, the roles were checked only when a group was created or its LLMs were reassigned, so changing the role of an LLM that was already in a group could leave several LLMs with the same role. The same aspects and concepts were then routed to each of these LLMs, and the extraction failed with an unrelated error (that they are already processed) after requests were sent. - Fallback LLMs now apply their own
num_retries_failed_requestandmax_retries_failed_requestduring extraction. Previously, these were ignored for the fallback LLM, which failed the extraction on its first API error, e.g. a single rate limit error. - A request that is sent again because the LLM returned invalid data (
max_retries_invalid_data) is now retried on transient API errors as any other request, up tonum_retries_failed_requesttimes. Previously, such a request was sent without retries, so a single transient error on it, e.g. a rate limit error, failed the extraction with an API error. - A request that failed with an API error is no longer sent to the same LLM again during extraction. Previously, when a fallback LLM was configured or
raise_exception_on_extraction_error=Falsewas passed, such a request was repeated up tomax_retries_invalid_datatimes without a delay, e.g. right after a rate limit error.max_retries_invalid_datanow applies only when the LLM returns invalid data. - With concurrency enabled, an API error that fails the extraction is now raised as
ModelAPIErrorright away. Previously, the failed request was sent again first, and the error was raised asModelExtractionErrorifmax_retries_invalid_data=0was set. - With
raise_exception_on_extraction_error=False, an LLM that hasmax_retries_invalid_data=0and no fallback models now issues a single warning for a call that fails because its API request failed, or because its response was cut off at the max output tokens: the warning about that failure. The warning that the LLM returned invalid JSON, which is wrong for such a call, is no longer issued for it. - With concurrency enabled, requests to a fallback model are now paced by the
async_limiterof that fallback model. Previously, theasync_limiterset on a fallback LLM had no effect: requests to the fallback LLM shared the limiter of the primary LLM. - With concurrency enabled, all the requests to a model that are sent in the same event loop are now paced by the same limiter, at the rate of the
async_limiterof the model. Previously, each extraction call had a limiter of its own, so the calls that run concurrently, e.g. for the sub-aspects and for the concepts of different aspects inextract_all(), each sent requests at the full rate, and together exceeded the rate several times. The same applied to the extractions run concurrently with the async methods, e.g. from several documents. An extraction with concurrency may take longer now: setasync_limiterto the rate limits of your provider account. - With
auto_pricing=True, the input tokens read from and written to the prompt cache of the LLM provider are now priced at the cache prices of the model. Previously, all input tokens were priced as regular input tokens, which overestimated the cost of the requests served from the cache, e.g. with the automatic prompt caching of OpenAI. - With
auto_pricing=True, each LLM request is now priced on its own. Previously, the tokens of several requests were summed up before being priced, e.g. when a document was processed in chunks or a chat response took several tool rounds. For the models priced higher above a certain number of input tokens per request (e.g. 200K), the sum could exceed that number even though no single request did, in which case all the tokens were priced at the higher prices. - With
auto_pricing=True, the costs of a model are now calculated with the prices of the model that is set when its requests are sent. Previously, the prices were looked up only when the model was created: after another model was assigned tomodel, the costs were still calculated with the prices of the previous one, and a model that was assignedauto_pricing=Trueafter it was created had its costs left at zero, without a warning. The warning that the automatically looked-up prices are not exact is now also issued whenauto_pricing=Trueis assigned. - The warning that
output_languagetakes no effect with a custom system message is now issued only whenoutput_languageis set to another value than its default ("en"). Previously, it was issued for every LLM with a custom system message, includingsystem_message="", which the warning on the default system message suggests for chat. - The warnings on the configuration of a model (the output language, the tools and the automatic pricing) are now attributed to the line of the calling code that creates the model, assigns a value to its parameter, or deserializes it. Previously, they were attributed to a line of Pydantic or of ContextGem, which was shown with the warning instead of the line that caused it, and which a warning filter by module had to name.
- Importing ContextGem on Python 3.14 no longer issues a
DeprecationWarningforasyncio.iscoroutinefunction(), which is to be removed in Python 3.16, e.g. in the output of a test run. - Token usage is now tracked for every model. Previously,
get_usage()reported zero input and output tokens for an LLM that had neitherpricing_detailsnorauto_pricingset, while the calls of that LLM were still recorded. - A model can now be deserialized if its fallback models differ from it, or from each other, only in
api_keyorapi_base, e.g. the same model served from another endpoint. Previously, such an LLM could be created and serialized, but deserializing it failed with a validation error: as the API credentials are not included in the serialized data, the fallback LLM was taken for an exact copy of the primary LLM. A fallback model with the same configuration as the primary model or as another fallback model is still rejected when the fallback models are set, i.e. when the primary model is created with them orfallback_modelsis assigned. This is no longer checked when another parameter of the primary model is assigned, so that the API credentials of a deserialized model and of its fallback models can be set again in any order. - An instance is now left unchanged when a value assigned to one of its parameters is rejected by a check that involves its other parameters. Previously, the error was raised but the rejected value was kept, i.e. the instance was left in the state that the error reported as invalid. This affected, among others, the role, output language, and fallback LLMs of a
DocumentLLM, the output language of aDocumentModelGroup, the examples of aJsonObjectConcept, the sentences of a paragraph, and the sub-aspects of an aspect. - In chat with tools, tool calls that the LLM returns together with a text in the same response are now executed for the models that litellm calls through the OpenAI Responses API, e.g. OpenAI reasoning models (
gpt-5.4and later) that are given function tools. Previously, such tool calls were ignored, andchat()returned the text that came with them as the final answer, without calling the remaining tools. - In chat with tools, an assistant turn in which the LLM requests tool calls without returning a text is now sent back to the LLM without a text. Previously, the placeholder text
<tool_call_response>was sent as the text of such turns. Some LLMs repeated it in their responses, and with some chat templates (e.g. Mistral models on Ollama) it replaced the tool calls in the conversation rendered for the LLM, so that the LLM stopped calling the remaining tools. Thecontentof a chat message is therefore optional: it isNoneonly for such assistant turns, which are stored separately fromChatSession.messages. - The function passed to
toolsis now the one that is called when the LLM uses the tool. Previously, the function was looked up by its name in the calling code, so a tool passed as a module attribute (tools=[my_tools.search]) or imported under another name was rejected with "No registered handler found". - In the schema of a tool, a
TypedDictfield marked asNotRequiredis no longer listed as required when the annotations of the module are evaluated lazily (from __future__ import annotations). - The documentation no longer describes passing a custom tool schema to
@register_tool(schema=...): this was not supported, and raisedTypeError. - A tool whose type hints cannot be resolved, e.g. because they use a type defined inside a function in a module with
from __future__ import annotations, is now reported with an error that names the type that was not found and how to fix it. Previously, the error stated that a supported type hint, e.g.list[...], was not supported. - The tools of an LLM are no longer used during extraction. Previously, the tools, together with
tool_choiceandparallel_tool_calls, were sent with the requests of an extraction, and the tool calls returned by the LLM were executed, although tools are documented, and warned about, as ignored by the extraction methods. - In chat with tools, an error in a tool function is no longer handled as an error of the LLM API. A tool that returns a value other than a string now raises
TypeErrorfromchat()right away. Previously,ModelAPIErrorwas raised instead ("Exception occurred while calling LLM API"), and if fallback LLMs were configured, each of them was tried first, calling the tool again. - In chat with tools, when the LLM calls a tool that does not exist, it now receives an error with the names of the available tools, and can correct the call. Previously, such a call failed the request with
ModelAPIError, or passed the request to a fallback LLM. - In chat with tools, no tool is called when a response of the LLM has a tool call that cannot be answered, i.e. a tool call without the name of the tool or without the ID of the call. Previously, the tools of the other tool calls in that response, and the tool of a call without an ID, were called before the request failed.
- In a chat session, the tool calls of each answered turn and their results are now kept in the history of the session, between the prompt of the turn and its answer, and are sent to the LLM with the next turns. Previously, they were left out of the history: in the next turns, the LLM saw its earlier answers without the tool results they were based on, so it could call the tools again to check an answer, and could not answer a question about an intermediate result. The tool calls of a turn that ends without an answer, e.g. on an error of the LLM API, are not kept, i.e. the turn is started over when it is sent again, as is the case when a fallback LLM takes over a turn.
ChatSession.messagesstill lists only the messages of the user and the answers of the LLM, and the history, with the tool calls, is kept when the session is serialized or cloned. - The usage and cost of the requests already sent are now recorded when a chat with tools, or an extraction that takes several requests (e.g. with
max_paragraphs_to_analyze_per_callset), ends with an error: whentool_max_roundsis reached, when a request fails after other requests succeeded, or when a chat is cancelled, e.g. on a timeout set by the calling code. Previously, the tokens used by such requests were not counted. The request that failed is now recorded as a call without a response also when the error is raised, rather than only when a warning is logged instead or a fallback LLM is tried. A request that failed after a round of tool calls no longer records the first request of the chat twice. - A
seedof0set forDocumentLLMis now sent with the requests. Previously, it was left out, as if no seed was set. - An aspect that is extracted again with
overwrite_existing=Truenow has the reference paragraphs and sentences of its newly extracted items only. Previously, the references of the earlier extraction were kept, so the sub-aspects and the concepts of the aspect that were extracted afterwards were also given the paragraphs that were no longer extracted for the aspect. - An aspect or a concept that is passed to an extraction method more than once, e.g.
from_concepts=[concept, concept], is now extracted once, and is returned once. Previously, without concurrency, it was listed in the prompt as many times as it was passed, and got the items of each listing, i.e. the same items several times. With concurrency, a request was sent for each time it was passed, and the extraction then failed with aValueErroron the aspect or the concept being already processed. The sub-aspects of an aspect passed more than once were extracted for each time as well. - With concurrency enabled, when the extraction with one model of a
DocumentModelGroupfails, the extraction with the other models of the group is now stopped before the error is raised: their requests in flight are cancelled, and no requests are sent afterwards. Previously, the other models kept sending requests, and updating the aspects and concepts, in the background after the calling code received the error. The same applies to whatever else is run concurrently, e.g. the extraction of the sub-aspects of several aspects, and the tools of a chat called in parallel: when a tool fails the chat by returning a value other than a string, the tools still running are cancelled. The usage of the requests completed by then is recorded. - The
api_keyof a model is no longer shown when the model is printed (repr(),str()), on its own or as a part of another object, e.g. in logs, or when what theerrors()method of a validation error returns is printed, for an input that includes the model, such as thefallback_modelsof another model or themodelsof aDocumentModelGroup. Thejson()method of such an error writes the model out without the key. Previously, the key was shown and written there in clear text. - The text of a validation error of a
DocumentLLM, aDocumentDecisionModelor aDocumentModelGroupno longer shows the values that failed the validation (input_value=..., input_type=...), so that no part of anapi_keycan appear there, e.g. in logs. The error still names the invalid parameter and the reason, and the values are still returned by theerrors()andjson()methods of the error, unlessinclude_input=Falseis passed. A model that is among the values is returned byerrors()as it is, i.e. with itsapi_keyas an attribute, and is written out byjson()with all its parameters except theapi_key. Anapi_keythat is among the values as it was passed, and not as a part of a model, is returned by both methods as'**********': a key that was rejected itself, e.g. one of a wrong type, and the key among the parameters that a model is created with or deserialized from, which are the value that the error reports when a required parameter is missing or a check that involves several parameters fails. The value of a parameter that a model or a group does not have is replaced in the same way, whatever it is, as it may be the key passed or assigned under a misspelled name (apikey=key), or passed to aDocumentModelGroup, which has no such parameter. A parameter that is set toNoneis reported asNone, the other values are returned as they were passed, and the rest of the error (the number and the order of the errors, their types, locations and messages) is not changed. A key that is passed as the value of another parameter, e.g. ofmodel, cannot be told from any other value and is returned as it is. Previously, the text of the error showed anapi_keythat was rejected itself, and the beginning or the end of the key passed as the first or the last parameter of a model, when a check that involves several parameters failed, anderrors()andjson()returned the key in clear text in all these cases. - A tab, a line break or another whitespace character between two words is now replaced with a space when a text is prepared for a prompt of an LLM or for a request to a decision model. Previously, such a character was removed together with the control characters, and the words around it were joined:
Fee:<tab>5,000 EURwas sent asFee:5,000 EUR, and a paragraph or a sentence with a line break inside it, e.g. one segmented with a SaT model or passed as aParagraph, was sent with the last word of a line joined to the first word of the next line. The same applies to the form feed, the Unicode line and paragraph separators, and the narrow no-break space. Line breaks are kept, as before, in the text of a document that is sent as a whole. A text is sent unchanged where no words were joined. - A negative
max_paragraphs_to_analyze_per_callormax_images_to_analyze_per_callpassed to an extraction method now raisesValueErrorbefore any requests are sent. Previously, an LLM was sent no requests and extracted nothing, without an error, when such a value applied to what it was to extract (extract_all()then failed with an unrelated error on an aspect that is not yet processed). - A concept with references to sentences (
add_references=Truewithreference_depth="sentences") is now extracted from an aspect also when the sentences of the document are segmented only after the aspects were extracted, e.g. when the concept is attached to the aspect afterextract_aspects_from_document()was called. Previously, for such a concept of a sub-aspect,extract_concepts_from_aspect()failed with an error that the sub-aspect must be assigned to the document, and succeeded when called again. For such a concept of an aspect of a document restored from its serialized form (from_dict(),from_json(),from_disk()), the prompt had no text of the paragraphs of the aspect. The paragraphs of a sub-aspect, and of the aspects of a restored document, were copies of the paragraphs of the document, and they did not get the sentences. - The reference paragraphs and reference sentences of the aspects of a document, of their sub-aspects, and of the extracted items of the aspects and concepts, are now the paragraphs and sentences of the document itself, i.e. the same objects as in
document.paragraphs, after each extraction method and in a document restored from its serialized form (from_dict(),from_json(),from_disk()). Previously, this held only for what was extracted directly from the document: the sub-aspects, the extracted items of the concepts of sub-aspects, everything in a restored document, and the aspects and concepts that a processed document already had when others were assigned to it (e.g. withadd_aspects()) referenced equal copies with the same unique IDs. The copies took memory, as a restored document held a separate copy of a paragraph, with its sentences, for each reference to it, and went stale: what was changed in a paragraph of the document afterwards, e.g. itscustom_data, did not reach them. Aspects and concepts are still copied when they are assigned, so what is assigned to a processed document or aspect references copies until an extraction method is called on the document. The serialized data is not changed. See the migration guide. - The references of the extracted items of an aspect are now added to the references of the aspect by their unique IDs. Previously, a paragraph or a sentence that the aspect already referenced as an object with the same unique ID and other content, e.g. a copy without the sentences that were segmented later, was taken for another reference:
ValueErrorwas raised for duplicates, and the duplicate was left among the references of the aspect. - The concepts of a sub-aspect are no longer replaced by copies when they are extracted: a concept taken from
sub_aspect.conceptsbeforeextract_concepts_from_aspect()orextract_all()is the one that has the extracted items afterwards, as for the concepts of an aspect. - With concurrency enabled, an error raised in one of the calls of an extraction, other than a failure of the model itself (a failed API request, a response cut off at the max output tokens, invalid data), is now raised as it is, as without concurrency. Such an error is e.g. the
ValueErrorfor a prompt that exceeds the context window of an LLM with no fallback LLM left to try, formax_tokensabove the limit of the model, or for text concepts of a document that has no text. The other calls are stopped before the error is raised: their requests in flight are cancelled, no requests are sent afterwards, and the usage of the requests completed by then is recorded. Previously, when the aspects or concepts were extracted in several calls, such an error was handled as invalid data returned by the LLM: the call was made again, by the LLM or by its fallback LLM, before the error was raised. An LLM withmax_retries_invalid_data=0and no fallback LLM failed withModelExtractionErrorreporting invalid JSON instead, or only issued a warning about invalid JSON ifraise_exception_on_extraction_error=Falsewas passed, even when no request had been sent. - The
reference_sentencesof an aspect withreference_depth="sentences"are now the sentences extracted for the aspect, i.e. the reference sentences of its extracted items, in the order of the items, each once. Previously, they were all the sentences of each reference paragraph of the aspect, including the sentences that were not extracted: a paragraph of five sentences of which one was extracted gave five reference sentences. Thereference_sentencesof the extracted items and thereference_paragraphsof the aspect are not affected, and the sub-aspects and the concepts of the aspect are extracted from its reference paragraphs, as before. - An ID of a sentence that is not a number, in the output of an LLM for an aspect with
reference_depth="sentences", is now handled as any other invalid data returned by the LLM: the call is made again, up tomax_retries_invalid_datatimes, andModelExtractionErroris raised, or a warning is logged, if the data stays invalid. Previously, such output raisedValueErrorright away, without the call being made again, unless the aspects were extracted in several concurrent calls. - An ID of a paragraph or of a sentence that an LLM lists more than once, among those of an aspect or among the references of an extracted item of a concept, is now used once, as the LLM is instructed to list each ID once: the aspect gets one extracted item for the paragraph or the sentence, and the item of the concept references it once. Previously, an aspect got an extracted item for each time that the ID was listed, and for a concept,
ValueErrorwas raised. - The usage and cost of a request to an LLM are now recorded also when an error is raised after the response to it was received, e.g. the
ValueErrorfor an aspect or a concept that another extraction of the same document processed while the request was in flight. This applies with and without concurrency, to the requests that are sent again because the LLM returned invalid data, to the requests to fallback LLMs, and to the LLMs of a model group. Previously, such a request was not recorded:get_usage()andget_cost()had neither its call, nor its tokens, nor its cost, although the request was sent and answered. - With concurrency enabled, when one of the calls of an extraction fails for good, the other calls of the same model are now stopped before the error is raised: their requests in flight are cancelled, no requests are sent afterwards, the aspects and concepts extracted by then keep their items, and the usage of the requests completed by then is recorded. A call fails for good when its API request fails, or its response is cut off at the max output tokens, and there is no fallback model left to try (
ModelAPIError), or when the model returned invalid data for it and the call is not made again (ModelExtractionError): the data is still invalid after the last retry, or the model has no fallback model and is not asked again for invalid data, i.e. it is an LLM withmax_retries_invalid_data=0or a decision model. Previously, the other calls were completed before the error was raised, including the calls whose requests were not sent yet, e.g. as they waited for theasync_limiter, so that e.g. with an invalid API key a request was still sent for each aspect or concept. Nothing changes when the failure does not fail the extraction: the other calls go on when a fallback model is yet to be tried, and whenraise_exception_on_extraction_error=Falseis passed. - A request to an LLM that is cancelled while it waits for the
async_limiter, e.g. on a timeout set by the calling code, no longer leaves a coroutine that was never awaited. Previously, Python issued aRuntimeWarningfor each such request ("coroutine ... was never awaited"). - The log of an extraction now tells what happened to a call that failed: a failed API request, a prompt that exceeds the context window of the LLM, and a response cut off at the max output tokens are each reported as such, and once. Previously, when the API request of an LLM failed, and a fallback LLM was configured or
raise_exception_on_extraction_error=Falsewas passed, the log also reportedLLM did not return valid JSON, although the LLM returned nothing. The first call of a fallback LLM was logged as a retry for invalid data (e.g.Retrying concepts with invalid JSON (1/3)), whatever the failure it was tried for, and a request that still failed for unsupported params after they were dropped was not logged as failed. The retries of a fallback LLM for invalid data are now numbered among its own retries. Only log lines are changed: the exceptions and the warnings are the same. - The
prompt_typeparameter ofDocumentLLM._update_default_prompt()is now typed with the values that the method accepts,"aspect"and"concept". Previously, it was typed with"aspects"and"concepts", which the method rejects withValueError, so that a type checker reported the calls that work. - A request that still fails for its params when it is sent again with the unsupported params dropped is now reported as any other failed request is: one warning is logged for it when
raise_exception_on_extraction_error=Falseis passed and there is no fallback model left to try. Previously, a warning was issued for it also when a fallback LLM took over and whenModelAPIErrorwas raised, and two warnings when they were issued instead of the exception. - The
json.JSONDecodeErrorthatfrom_json()raises for a string that is not a valid JSON, andfrom_disk()for a file that holds one, no longer holds the string. Previously, the whole string was kept in thedocattribute of the error, e.g. the data of an LLM with an API key that was added to it. The message of the error, with the position of what is invalid, is unchanged. from_dict()andfrom_json()now raiseValueErrorfor data without the class name, e.g. an empty JSON object or a JSON array, as for the class names of the nested objects. Previously, they raisedKeyError, orTypeErrorfor a JSON value other than an object.- The concepts of a sub-aspect are now extracted also when it takes more than one call. Previously,
extract_concepts_from_aspect()andextract_all()raisedValueError: Aspect ... must be assigned to the documentwhen a second call was made for the same sub-aspect: without concurrency, whenmax_items_per_callput its concepts into several calls or the concepts differed in their extraction params, e.g. one of them was extracted with justifications; with concurrency, when a call was made again, e.g. as the LLM returned invalid data for it. - A fallback LLM that returns invalid data during extraction is now asked again up to its
max_retries_invalid_datatimes after its first call, i.e. it is called up to1 + max_retries_invalid_datatimes, as a primary LLM is. Previously, the first call of a fallback LLM was counted as one of its retries: withmax_retries_invalid_data=3(the default), it was called three times, while a primary LLM was called four times, and withmax_retries_invalid_data=1it was not asked again at all. A fallback LLM withmax_retries_invalid_data=0is called once, as before, and so is a fallback decision model. The number of retries reported when the data stays invalid, inModelExtractionError.retry_count, in the text of the error ((after N retries)) and in the warning logged instead withraise_exception_on_extraction_error=False, is now the number of times that the last model tried was called again. Previously, the first call of a fallback LLM was counted there as a retry, so that one retry was reported for a fallback LLM withmax_retries_invalid_data=0, which was called once. Nothing changes for the primary LLM, nor for a failed API request or a response cut off at the max output tokens, which are not requested again. See the migration guide. - With the logger disabled (
CONTEXTGEM_LOGGER_LEVEL=OFF), the non-actionable log messages oflitellmare now hidden, as they are at any other level. Previously, the filter that hides them was installed only when the logger was enabled, so with the logger disabled before ContextGem was imported such messages were shown, e.g. a warning oflitellmon each call of a sync method after the first one. CONTEXTGEM_SUPPRESS_LITELLM_NOISE=0, which disables the filter that hides the non-actionable log messages oflitellm, now also takes effect when it is set after ContextGem was imported andreload_logger_settings()is called: the filter is removed, and is installed again once the variable is no longer set to0. Previously, the variable only kept the filter from being installed, and a filter that was already installed stayed.- An ID of an aspect, a concept, a paragraph or a sentence in the output of an LLM is now read only if it is one of the IDs that the prompt shows, e.g.
A0,C1,P3orP3-S1, with or without surrounding whitespace, and a sentence ID only for its own paragraph. Any other ID is handled as any other invalid data returned by the LLM: the call is made again, up tomax_retries_invalid_datatimes, andModelExtractionErroris raised, or a warning is logged, if the data stays invalid. Previously, an ID was read by removing its prefix and converting the rest to a number, which also read IDs that the prompt does not show:P1_0was taken for the paragraphP10,PP3,P+3,P03andP٣(with an Arabic-Indic digit) forP3, and0for the aspectA0, while an ID with whitespace before it, e.g.A0after a space, was rejected. A sentence ID of another paragraph than the one that the LLM listed it for, e.g.P4-S1listed for the paragraphP3, was taken for the sentence of the listed paragraph with that number (P3-S1). An LLM that writes the IDs in another form than the prompt shows, e.g.P03or3forP3, is now asked again. - A value of another type than the concept expects, returned by an LLM for a date concept (e.g. a number or
nullinstead of a date string) or for a rating concept (e.g. the string"3"instead of the number 3), is now handled as any other invalid data returned by the LLM: the call is made again, up tomax_retries_invalid_datatimes, andModelExtractionErroris raised, or a warning is logged, if the data stays invalid. Previously, such a value failed the extraction withAttributeErrororTypeError, without the call being made again. - The output of an LLM must now be valid JSON also in its numbers:
NaN,Infinityand-Infinity, which JSON does not have, and a number too large for a float (e.g.1e400) make the output invalid data, for which the call is made again, up tomax_retries_invalid_datatimes. Previously, they were read as floats that are not finite, so that e.g. a numerical concept could get an item with the valuenanorinf, whichto_json()then wrote asNaNorInfinity. An integer with more digits than Python converts (4,300 by default), and arrays or objects nested deeper than the JSON parser of Python can recurse, in the output of an LLM, are handled in the same way. Previously, they failed the extraction withValueErrororRecursionError. - The
additional_contextof paragraphs and sentences is now cleaned before it is sent to an LLM, as their text is, and as it already was for decision models: control characters are removed, and whitespace, line breaks included, is collapsed to single spaces. Additional context of which nothing is left is not sent. Previously, it was put in the prompt as it was, so that e.g. a line break in it could break the structure of the prompt. The prompt now also tells the LLM about additional context only when it shows some. Previously, it did so also for additional context that it did not show: that of sentences when the sentences are not listed (reference_depth="paragraphs"), and that of a paragraph without text to show. The warning for additional context with line breaks now says that they are replaced with spaces. - An output of an LLM that leaves out an aspect or a concept of the prompt is now handled as any other invalid data returned by the LLM: the call is made again, up to
max_retries_invalid_datatimes, andModelExtractionErroris raised, or a warning is logged, if the data stays invalid. The prompt asks for an entry for each aspect and concept, with an empty list if nothing is extracted for it, which stays a valid answer. Previously, an aspect or a concept that the output left out was left unprocessed, without items and without a warning or an error, so that it could not be told from one for which nothing was found, other than by its being extracted again by the next extraction call. - A concept with
singular_occurrence=Truenow has one extracted item at most, as documented: an output of an LLM with more than one item for such a concept, in one entry or in several entries for it, is handled as any other invalid data returned by the LLM, i.e. the call is made again. No item, when there is nothing to extract, stays a valid answer. Previously, all the items that the LLM returned for the concept in one response were kept. The documentation ofsingular_occurrencenow says "at most one item" where it said that a single item will be extracted. - A
JsonObjectConceptwith a field named like an attribute of a Pydantic model, e.g.json,copy,schema,dictorvalidate, no longer issues a Pydantic warning ("Field name ... shadows an attribute in parent BaseModel") each time a value is validated, i.e. for each extracted item and each example. A field namedmodel_configis now validated as any other field. Previously, no value was accepted for it, so that the concept could not be extracted. - A
JsonObjectConceptwith a mapping whose keys are not strings, e.g.dict[int, str], at any depth of its structure, is now rejected withValueErrorwhen it is created, as the keys of a JSON object are strings. Previously, such a structure was accepted, while the LLM output for it, from JSON, was always rejected as invalid data, except for an empty mapping. A mapping with keys of aLiteralof strings, e.g.dict[Literal["a", "b"], int], is now also accepted in a structure given as classes (JsonObjectClassStruct), as it already was in a structure given as a dictionary. - A
Literalin the structure of aJsonObjectConceptnow accepts a value only of the type of its own value: e.g.Literal[1]no longer acceptstrueor1.0, andLiteral[True]no longer accepts1. Previously, Pydantic accepted a value that is equal to a value of theLiteral, and the extracted item kept the value of the other type. - A value of a
JsonObjectConceptmay now have an empty object nested in it where the structure allows it: the value of a mapping field with no entries (e.g.{"scores": {}}fordict[str, int]), or of a nested structure whose fields are all optional. Previously, such a value was rejected as invalid data, so that the LLM was asked again, and the extraction failed when it returned it each time. The value itself must still not be empty. - The distributions declare
LICENSEandNOTICEas their license files, and no other file. Previously, the default pattern of the build backend also tookLICENSES.md, the license audit of the dependencies, andlicenseal.review.toml, the review file of the audit tool, which are not licenses of the package, into the sdist and into the license files of the wheel. - The default system message of a
DocumentLLMnow follows a change ofoutput_languageafter the LLM is created, also throughDocumentModelGroup.group_update_output_language(): it is rendered for the new output language, as for an LLM created with it. Previously, it kept the output language that the LLM was created with: changed to"adapt", the LLM was not told by its system message to write in the language of the document, as the extraction prompts did tell it, and a warning wrongly said thatoutput_languagewould take no effect; changed back to"en", the system message still told the LLM to write in the language of the document, while the extraction prompts told it to write in English. A custom system message, including an empty one, is kept as it is. - The warning issued when
output_languageis set together with a system message that is not the default one now says what is the case: the output language applies to the extraction prompts, while the system message gives no instruction on it, and in a chat it has no effect. Previously, the warning said thatoutput_languagewould take no effect, and the documentation that it applies only with the default system message, while the extraction prompts follow it whatever the system message is. - A SaT model in a local folder (
sat_model_idset to the path of the folder) is now loaded without network access, as the documentation said, if the folder also has the tokenizer of the model (tokenizer.json). Previously, the tokenizer was always taken from Hugging Face, also for a local model, so that without network access and with no tokenizer in the cache ofhuggingface_hubthe model failed to load, with an error that the folder does not contain a valid SaT model. A folder withouttokenizer.jsontakes the tokenizer from Hugging Face, or from the cache, as before, and the error now names the tokenizer as a possible cause. The documentation now describes when a SaT model is used, and how to use one where Hugging Face cannot be reached: from a cache filled in advance, e.g. when a container image is built, or from a local folder.