fast-completions 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/lib/fast/completions/constrained_sampling.rb +191 -0
- data/lib/fast/completions/dialects.rb +5 -1
- data/lib/fast/completions/errors.rb +14 -0
- data/lib/fast/completions/protocol.rb +23 -9
- data/lib/fast/completions/tool.rb +22 -0
- data/lib/fast/completions/version.rb +1 -1
- data/lib/fast/completions.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5741f8a7684abbb66479904f8b49d14afebfd783cf3069e2e2c91717a0765238
|
|
4
|
+
data.tar.gz: 1a20474b48f9278d815b1561101a34cc229ac37c1cfcaeb83ffe9f1ba5bbcf9a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bae0596f622cdee7289c271939aca77dc8e10d539aeed94b8c18388ad445957db4b19beb45a6359da7c971d3173d24e42ca917720499cfd081b398fd4e70f3ec
|
|
7
|
+
data.tar.gz: 30d7cc0ac12964163ca196e3e6a87e2e9e150de3db334ad342d55d923b55f0c522073108def6618c727a78593c5fb5d67f90d2e6d3267e794753575d9ab36bfe
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
Notable changes to the fast-completions gem. The format is prose-first,
|
|
4
4
|
one section per released version, newest first.
|
|
5
5
|
|
|
6
|
+
## 0.4.0 (2026-10-14)
|
|
7
|
+
|
|
8
|
+
- Strict-sampling tool schemas (pi constrained-sampling, JSON-schema half):
|
|
9
|
+
tools declare `constrained_sampling` — `false` (explicit opt-out) or
|
|
10
|
+
`{"type" => "json_schema", "strict" => "prefer"|"require"}`; any other
|
|
11
|
+
shape raises `ArgumentError` at declaration (grammar-constrained sampling
|
|
12
|
+
stays out of scope — `type: "grammar"` raises). On a dialect declaring
|
|
13
|
+
`supports_strict_mode`, a declared tool ships the strict rewrite of its
|
|
14
|
+
parameters — every property required, non-required null-less properties
|
|
15
|
+
wrapped in an `anyOf` of the property and `{"type": "null"}`,
|
|
16
|
+
`additionalProperties: false` appended — with `"strict": true` right after
|
|
17
|
+
`parameters`; every other function tool on a strict dialect ships
|
|
18
|
+
`"strict": false` with parameters verbatim (the declared Hash is never
|
|
19
|
+
mutated — the rewrite deep-copies); tools on non-strict dialects carry no
|
|
20
|
+
strict key ever, parameters verbatim. `"prefer"` falls back to verbatim
|
|
21
|
+
silently when the schema is unsupportable; `"require"` raises the new
|
|
22
|
+
`ConstrainedSamplingError` with pi's message templates. The Dialect
|
|
23
|
+
vocabulary grows to 14 keys (`supports_strict_mode`, default `false` —
|
|
24
|
+
OpenAI compatibility alone does not imply strict tool support); the
|
|
25
|
+
shipped `:zai` declares `true` (pi's glm compat).
|
|
26
|
+
|
|
6
27
|
## 0.3.0 (2026-10-15)
|
|
7
28
|
|
|
8
29
|
- Mid-stream salvage: a failure after a 2xx status — a mid-body connection
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fast
|
|
4
|
+
module Completions
|
|
5
|
+
# JSON-schema constrained sampling (I63) — the strict rewrite that lets
|
|
6
|
+
# a declared tool ship forced property emission on a strict dialect.
|
|
7
|
+
# Port of pi packages/ai/src/api/constrained-sampling.ts @ f1b2e77f5
|
|
8
|
+
# (the JSON-schema half; the grammar half is deliberately NOT ported —
|
|
9
|
+
# nothing half-anticipates grammar). Message bytes are pi's literals
|
|
10
|
+
# verbatim: they are the parity observable. Check order inside
|
|
11
|
+
# make_json_schema_node_strict is pi's exactly — which message throws
|
|
12
|
+
# first is observable behavior.
|
|
13
|
+
module ConstrainedSampling
|
|
14
|
+
# pi constrained-sampling.ts :17-34 (16 keys — the frozen order IS
|
|
15
|
+
# the first-present-key-wins order). NOTE: the iteration Grounds say
|
|
16
|
+
# "20 keys"; pi's array holds 16 and the Grounds enumerate the same
|
|
17
|
+
# 16 in the same order — the count numeral in the brief is wrong,
|
|
18
|
+
# pi is the oracle.
|
|
19
|
+
UNSUPPORTED_STRICT_SCHEMA_KEYS = [
|
|
20
|
+
"$ref",
|
|
21
|
+
"$defs",
|
|
22
|
+
"definitions",
|
|
23
|
+
"allOf",
|
|
24
|
+
"oneOf",
|
|
25
|
+
"patternProperties",
|
|
26
|
+
"dependentSchemas",
|
|
27
|
+
"dependencies",
|
|
28
|
+
"unevaluatedProperties",
|
|
29
|
+
"propertyNames",
|
|
30
|
+
"contains",
|
|
31
|
+
"prefixItems",
|
|
32
|
+
"not",
|
|
33
|
+
"if",
|
|
34
|
+
"then",
|
|
35
|
+
"else",
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
# pi :156-178. `true` (strict) or `nil` (loose). Only
|
|
41
|
+
# UnsupportedStrictJsonSchemaError is caught — any other error
|
|
42
|
+
# propagates (pi's rethrow).
|
|
43
|
+
def resolve_json_schema_strict_sampling(tool, supports_strict_mode)
|
|
44
|
+
config = tool.constrained_sampling
|
|
45
|
+
return nil if config.nil? || config == false || config["type"] != "json_schema"
|
|
46
|
+
|
|
47
|
+
if supports_strict_mode
|
|
48
|
+
begin
|
|
49
|
+
make_strict_json_schema(tool.parameters)
|
|
50
|
+
return true
|
|
51
|
+
rescue UnsupportedStrictJsonSchemaError => reason
|
|
52
|
+
return nil if config["strict"] != "require" # silent loose fallback
|
|
53
|
+
|
|
54
|
+
raise ConstrainedSamplingError,
|
|
55
|
+
"Tool \"#{tool.name}\" requires JSON-schema constrained sampling, but #{reason.message}."
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
if config["strict"] == "require"
|
|
59
|
+
raise ConstrainedSamplingError,
|
|
60
|
+
"Tool \"#{tool.name}\" requires JSON-schema constrained sampling, but strict tools are unsupported."
|
|
61
|
+
end
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# pi makeStrictJsonSchema :137-151: deep-copy FIRST (the caller's
|
|
66
|
+
# schema — nested included — is never mutated), then the root arms
|
|
67
|
+
# use the root-specific message (differs from the nested
|
|
68
|
+
# "boolean schemas…" arm), then the root type must be "object".
|
|
69
|
+
def make_strict_json_schema(schema)
|
|
70
|
+
cloned = deep_copy(schema)
|
|
71
|
+
raise UnsupportedStrictJsonSchemaError, "root schema must have type object" unless cloned.is_a?(Hash)
|
|
72
|
+
|
|
73
|
+
make_json_schema_node_strict(cloned)
|
|
74
|
+
raise UnsupportedStrictJsonSchemaError, "root schema must have type object" unless cloned["type"] == "object"
|
|
75
|
+
|
|
76
|
+
cloned
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# pi makeJsonSchemaNodeStrict :64-134, IN PLACE on the (cloned) node.
|
|
80
|
+
def make_json_schema_node_strict(node)
|
|
81
|
+
# JS isJsonSchemaObject: object, not null, not array. Everything
|
|
82
|
+
# else — booleans, null, arrays, scalars — throws here (a nil
|
|
83
|
+
# property value hits this arm: JS undefined equivalence).
|
|
84
|
+
raise UnsupportedStrictJsonSchemaError, "boolean schemas are unsupported" unless node.is_a?(Hash)
|
|
85
|
+
|
|
86
|
+
# JS `schema[key] !== undefined`: present-with-any-value (JSON null
|
|
87
|
+
# included) throws; Ruby key? is the exact equivalence.
|
|
88
|
+
UNSUPPORTED_STRICT_SCHEMA_KEYS.each do |key|
|
|
89
|
+
raise UnsupportedStrictJsonSchemaError, "#{key} schemas are unsupported" if node.key?(key)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# pi's optional isUnsupportedKeyword provider hook is NOT ported:
|
|
93
|
+
# pi's own call sites pass none (Recorded residual R2).
|
|
94
|
+
|
|
95
|
+
if node.key?("anyOf") # pi :76-89
|
|
96
|
+
raise UnsupportedStrictJsonSchemaError, "anyOf must contain at least one schema" if
|
|
97
|
+
!node["anyOf"].is_a?(Array) || node["anyOf"].empty?
|
|
98
|
+
|
|
99
|
+
node["anyOf"].each do |variant|
|
|
100
|
+
raise UnsupportedStrictJsonSchemaError, "object and array unions are unsupported" if structured_schema?(variant)
|
|
101
|
+
|
|
102
|
+
make_json_schema_node_strict(variant)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
if node.key?("items") # pi :91-96
|
|
107
|
+
raise UnsupportedStrictJsonSchemaError, "tuple schemas are unsupported" if node["items"].is_a?(Array)
|
|
108
|
+
|
|
109
|
+
make_json_schema_node_strict(node["items"])
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# JS `schema.type === "object"` — the string, strictly.
|
|
113
|
+
object_schema = node["type"] == "object"
|
|
114
|
+
if node.key?("properties") && !object_schema
|
|
115
|
+
raise UnsupportedStrictJsonSchemaError, "properties require type object"
|
|
116
|
+
end
|
|
117
|
+
return unless object_schema # non-object-typed nodes return here
|
|
118
|
+
|
|
119
|
+
# JS `!== false`: only exactly boolean false passes.
|
|
120
|
+
if node.key?("additionalProperties") && node["additionalProperties"] != false
|
|
121
|
+
raise UnsupportedStrictJsonSchemaError, "schema-valued or true additionalProperties is unsupported"
|
|
122
|
+
end
|
|
123
|
+
if node.key?("properties") && !node["properties"].is_a?(Hash)
|
|
124
|
+
raise UnsupportedStrictJsonSchemaError, "object properties must be a schema map"
|
|
125
|
+
end
|
|
126
|
+
if node.key?("required") &&
|
|
127
|
+
(!node["required"].is_a?(Array) || node["required"].any? { |key| !key.is_a?(String) })
|
|
128
|
+
raise UnsupportedStrictJsonSchemaError, "object required must be a string array"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
properties = node.key?("properties") ? node["properties"] : {}
|
|
132
|
+
property_names = properties.keys
|
|
133
|
+
required = node.key?("required") ? node["required"] : []
|
|
134
|
+
if required.any? { |key| !property_names.include?(key) }
|
|
135
|
+
raise UnsupportedStrictJsonSchemaError, "required contains an unknown property"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
properties.each do |key, property|
|
|
139
|
+
make_json_schema_node_strict(property)
|
|
140
|
+
if !required.include?(key) && !schema_allows_null(property)
|
|
141
|
+
# Reassignment keeps the key's hash position (JS and Ruby alike).
|
|
142
|
+
properties[key] = {"anyOf" => [property, {"type" => "null"}]}
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
node["required"] = property_names # existing key keeps its position
|
|
146
|
+
node["additionalProperties"] = false # absent key appends
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# pi schemaAllowsNull :54-60. The const arm needs the key-present
|
|
150
|
+
# check: JS `schema.const === null` is false when the key is absent
|
|
151
|
+
# (undefined ≠ null); Ruby `hash["const"].nil?` alone would read an
|
|
152
|
+
# ABSENT const as null-allowing and skip the wrap.
|
|
153
|
+
def schema_allows_null(schema)
|
|
154
|
+
return false unless schema.is_a?(Hash)
|
|
155
|
+
|
|
156
|
+
type = schema["type"]
|
|
157
|
+
return true if type == "null" || (type.is_a?(Array) && type.include?("null"))
|
|
158
|
+
return true if schema.key?("const") && schema["const"].nil?
|
|
159
|
+
return true if schema["enum"].is_a?(Array) && schema["enum"].include?(nil)
|
|
160
|
+
|
|
161
|
+
schema["anyOf"].is_a?(Array) && schema["anyOf"].any? { |variant| schema_allows_null(variant) }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# pi isStructuredSchema :42-51.
|
|
165
|
+
def structured_schema?(schema)
|
|
166
|
+
return false unless schema.is_a?(Hash)
|
|
167
|
+
|
|
168
|
+
types =
|
|
169
|
+
if schema["type"].is_a?(String) then [schema["type"]]
|
|
170
|
+
elsif schema["type"].is_a?(Array) then schema["type"]
|
|
171
|
+
else []
|
|
172
|
+
end
|
|
173
|
+
types.include?("object") || types.include?("array") ||
|
|
174
|
+
schema.key?("properties") || schema.key?("items")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# structuredClone's role: fresh containers all the way down, so the
|
|
178
|
+
# rewrite (property wraps, required/additionalProperties writes)
|
|
179
|
+
# never reaches the declared Hash. Leaves (strings, numbers, booleans,
|
|
180
|
+
# nil) are immutable in Ruby — no copy needed; frozen strings dup
|
|
181
|
+
# clean by simply being carried over.
|
|
182
|
+
def deep_copy(value)
|
|
183
|
+
case value
|
|
184
|
+
when Hash then value.each_with_object({}) { |(key, val), copy| copy[key] = deep_copy(val) }
|
|
185
|
+
when Array then value.map { |val| deep_copy(val) }
|
|
186
|
+
else value
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -6,7 +6,7 @@ module Fast
|
|
|
6
6
|
# the codec arms. Nothing here is provider detection, a catalog, or a
|
|
7
7
|
# base-URL constant — the consumer declares, the codec obeys (§4).
|
|
8
8
|
#
|
|
9
|
-
# The vocabulary is CLOSED to the
|
|
9
|
+
# The vocabulary is CLOSED to the 14 keys declared below. Unknown keys
|
|
10
10
|
# and unknown enum values raise ArgumentError AT DECLARATION TIME —
|
|
11
11
|
# fail loud, never silently mistranslate (the no-universal-translation
|
|
12
12
|
# discipline).
|
|
@@ -27,6 +27,9 @@ module Fast
|
|
|
27
27
|
supports_thinking_token_budget: [false, nil],
|
|
28
28
|
send_session_affinity_headers: [false, nil],
|
|
29
29
|
session_affinity_format: [:openai, %i[openai openai_nosession openrouter].freeze],
|
|
30
|
+
# I63 (pi detectCompat :1675): OpenAI compatibility alone does not
|
|
31
|
+
# imply strict JSON-schema tool support — data, not detection.
|
|
32
|
+
supports_strict_mode: [false, nil],
|
|
30
33
|
}.freeze
|
|
31
34
|
|
|
32
35
|
attr_reader :name
|
|
@@ -154,6 +157,7 @@ module Fast
|
|
|
154
157
|
supports_developer_role false
|
|
155
158
|
supports_reasoning_effort true
|
|
156
159
|
supports_store false
|
|
160
|
+
supports_strict_mode true # pi zai.json glm compat: supportsStrictMode:true
|
|
157
161
|
end
|
|
158
162
|
|
|
159
163
|
Dialects.declare(:deepseek) do
|
|
@@ -61,6 +61,20 @@ module Fast
|
|
|
61
61
|
|
|
62
62
|
class CancelledError < StandardError; end # NOT < Error (D8)
|
|
63
63
|
|
|
64
|
+
# I63 constrained sampling (pi api/constrained-sampling.ts). INTERNAL:
|
|
65
|
+
# a tool schema outside the strict subset pi's makeStrictJsonSchema
|
|
66
|
+
# accepts — the reason bytes are pi's literals verbatim. Never escapes
|
|
67
|
+
# the resolver: "prefer" falls back to loose on it, "require" converts
|
|
68
|
+
# it to the public ConstrainedSamplingError (pi rethrows only other
|
|
69
|
+
# error classes).
|
|
70
|
+
class UnsupportedStrictJsonSchemaError < Error; end
|
|
71
|
+
|
|
72
|
+
# PUBLIC: a strict:"require" tool whose schema cannot be strict-rewritten,
|
|
73
|
+
# or that runs on a dialect without supports_strict_mode — pi's two
|
|
74
|
+
# message templates verbatim (the inner reason carries no trailing
|
|
75
|
+
# period; the template adds it).
|
|
76
|
+
class ConstrainedSamplingError < Error; end
|
|
77
|
+
|
|
64
78
|
# Internal: error classification helpers (Specification §8).
|
|
65
79
|
module Errors
|
|
66
80
|
# pi utils/overflow.ts :33-96 — the FULL pattern list, ported
|
|
@@ -66,18 +66,32 @@ module Fast
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
# §9.8 / pi buildParams :853-863: tools iff declared (declaration
|
|
69
|
-
# order
|
|
70
|
-
#
|
|
71
|
-
#
|
|
69
|
+
# order). Parameters stay VERBATIM — the consumer's schema is the
|
|
70
|
+
# bytes — EXCEPT the I63 strict arm: a tool declaring
|
|
71
|
+
# constrained_sampling on a strict dialect ships the strict REWRITE
|
|
72
|
+
# (pi convertTools :1505-1516, getJsonSchemaToolParameters) and the
|
|
73
|
+
# function object carries "strict" right after "parameters" (pi key
|
|
74
|
+
# order: name, description, parameters, strict) — declared-strict
|
|
75
|
+
# true, undeclared/loose false. VERBATIM remains the discipline for
|
|
76
|
+
# undeclared tools and non-strict dialects: no strict key ever there,
|
|
77
|
+
# and the emitted parameters are the declared object itself
|
|
78
|
+
# (object-identity emission). zai tool_stream right after; the
|
|
79
|
+
# `tools: []` fallback when tool history exists with no declared
|
|
80
|
+
# tools.
|
|
72
81
|
def apply_tools(params, tools, messages, dialect)
|
|
73
82
|
if tools.any?
|
|
83
|
+
strict_capable = dialect.supports_strict_mode
|
|
74
84
|
params[:tools] = tools.map do |tool|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
strict = ConstrainedSampling.resolve_json_schema_strict_sampling(tool, strict_capable)
|
|
86
|
+
function = {
|
|
87
|
+
"name" => tool.name,
|
|
88
|
+
"description" => tool.description,
|
|
89
|
+
"parameters" => strict == true ? ConstrainedSampling.make_strict_json_schema(tool.parameters) : tool.parameters,
|
|
90
|
+
}
|
|
91
|
+
# pi :1513 — the explicit key on EVERY function tool when the
|
|
92
|
+
# dialect supports strict mode ("Some reject unknown fields").
|
|
93
|
+
function["strict"] = strict || false if strict_capable
|
|
94
|
+
{"type" => "function", "function" => function}
|
|
81
95
|
end
|
|
82
96
|
params[:tool_stream] = true if dialect.zai_tool_stream
|
|
83
97
|
elsif tool_history?(messages)
|
|
@@ -25,12 +25,34 @@ module Fast
|
|
|
25
25
|
end
|
|
26
26
|
@parameters || {"type" => "object", "properties" => {}}
|
|
27
27
|
end
|
|
28
|
+
|
|
29
|
+
# pi types.ts :736-754: constrainedSampling?: false |
|
|
30
|
+
# {type:"json_schema", strict:"prefer"|"require"}. Default nil
|
|
31
|
+
# (undeclared — no strict participation); false is the explicit
|
|
32
|
+
# opt-out, wire-equivalent to nil. ANY other shape raises here, at
|
|
33
|
+
# declaration time — fail loud, never silently mistranslate
|
|
34
|
+
# (unknown type — grammar included, the named exclusion — unknown
|
|
35
|
+
# strict value, missing keys, non-Hash values).
|
|
36
|
+
UNSET = Object.new.freeze # nil is NOT a passable value (raises below)
|
|
37
|
+
|
|
38
|
+
def constrained_sampling(config = UNSET)
|
|
39
|
+
return @constrained_sampling if config.equal?(UNSET)
|
|
40
|
+
|
|
41
|
+
unless config == false || (config.is_a?(Hash) &&
|
|
42
|
+
config["type"] == "json_schema" && %w[prefer require].include?(config["strict"]))
|
|
43
|
+
raise ArgumentError,
|
|
44
|
+
"constrained_sampling must be false or {\"type\" => \"json_schema\", " \
|
|
45
|
+
"\"strict\" => \"prefer\"|\"require\"}; got #{config.inspect}"
|
|
46
|
+
end
|
|
47
|
+
@constrained_sampling = config
|
|
48
|
+
end
|
|
28
49
|
end
|
|
29
50
|
|
|
30
51
|
# Instance readers forward to the class declaration.
|
|
31
52
|
def description = self.class.description
|
|
32
53
|
def tool_name = self.class.tool_name
|
|
33
54
|
def parameters = self.class.parameters
|
|
55
|
+
def constrained_sampling = self.class.constrained_sampling
|
|
34
56
|
|
|
35
57
|
# The wire name: the declared tool_name if set, else the class name (§11).
|
|
36
58
|
def name
|
data/lib/fast/completions.rb
CHANGED
|
@@ -11,6 +11,7 @@ require_relative "completions/types"
|
|
|
11
11
|
require_relative "completions/dialects"
|
|
12
12
|
require_relative "completions/sse"
|
|
13
13
|
require_relative "completions/codec"
|
|
14
|
+
require_relative "completions/constrained_sampling"
|
|
14
15
|
require_relative "completions/protocol"
|
|
15
16
|
require_relative "completions/client"
|
|
16
17
|
require_relative "completions/tool"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fast-completions
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eric Jacobs
|
|
@@ -38,6 +38,7 @@ files:
|
|
|
38
38
|
- lib/fast/completions/chat.rb
|
|
39
39
|
- lib/fast/completions/client.rb
|
|
40
40
|
- lib/fast/completions/codec.rb
|
|
41
|
+
- lib/fast/completions/constrained_sampling.rb
|
|
41
42
|
- lib/fast/completions/dialects.rb
|
|
42
43
|
- lib/fast/completions/errors.rb
|
|
43
44
|
- lib/fast/completions/protocol.rb
|