completion-kit 0.5.5 → 0.5.7
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/README.md +1 -1
- data/app/assets/javascripts/completion_kit/application.js +8 -4
- data/app/assets/stylesheets/completion_kit/application.css +201 -23
- data/app/controllers/completion_kit/prompts_controller.rb +1 -1
- data/app/controllers/completion_kit/runs_controller.rb +6 -0
- data/app/helpers/completion_kit/application_helper.rb +64 -10
- data/app/models/completion_kit/provider_credential.rb +8 -0
- data/app/services/completion_kit/worker_health.rb +4 -1
- data/app/views/completion_kit/api_reference/_authentication.html.erb +11 -0
- data/app/views/completion_kit/api_reference/_body.html.erb +311 -0
- data/app/views/completion_kit/api_reference/index.html.erb +6 -324
- data/app/views/completion_kit/prompts/_form.html.erb +6 -1
- data/app/views/completion_kit/prompts/index.html.erb +7 -0
- data/app/views/completion_kit/prompts/show.html.erb +65 -8
- data/app/views/completion_kit/provider_credentials/_models_card.html.erb +25 -20
- data/app/views/completion_kit/provider_credentials/index.html.erb +1 -0
- data/app/views/completion_kit/runs/_form.html.erb +2 -1
- data/app/views/completion_kit/runs/show.html.erb +1 -1
- data/app/views/completion_kit/tags/_picker.html.erb +1 -1
- data/app/views/layouts/completion_kit/application.html.erb +3 -2
- data/lib/completion_kit/version.rb +1 -1
- data/lib/completion_kit.rb +3 -0
- data/lib/generators/completion_kit/templates/initializer.rb +4 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 33daa5f930979539f0b757bd5ea85273b9620f90f3de0bf4d976c843cae24af5
|
|
4
|
+
data.tar.gz: 3387de72acf76fbeac7d79628144ac9cb3782678cf186ad883d1978bceb4daee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3af6ed91c4c791859b41cddf24078fc0ecec5a79a5aa6ead1ff996c07f9173164693bea12ac223443ae33fcadb1499919734c112a453260f4e9e187cbd6cfd37
|
|
7
|
+
data.tar.gz: cf05c70a638f3ba9c70657b9c4ab90154af4b5d15e44dbff80b00bd2fc5804e2f3c6313f22fba5283ab36d35adb6d218ec3e1792ee533191c1a7bb66eef59de6
|
data/README.md
CHANGED
|
@@ -142,7 +142,7 @@ Only one mode can be active.
|
|
|
142
142
|
|
|
143
143
|
## Concepts
|
|
144
144
|
|
|
145
|
-
- **Prompt.** A versioned template with `{{variable}}` placeholders.
|
|
145
|
+
- **Prompt.** A versioned template with `{{variable}}` placeholders. Editing a prompt that's already been run creates a new version, so earlier results stay reproducible.
|
|
146
146
|
- **Dataset.** A CSV of real inputs. Each row becomes one test case.
|
|
147
147
|
- **Run.** One execution of a prompt against a dataset. Captures every input (model, temperature, metrics) and stores all outputs and scores.
|
|
148
148
|
- **Response.** The model's output for one dataset row, with reviews attached.
|
|
@@ -105,11 +105,16 @@ document.addEventListener("mouseout", function(e) {
|
|
|
105
105
|
});
|
|
106
106
|
|
|
107
107
|
var ckRefreshing = false;
|
|
108
|
+
function ckSetRefreshButtonsBusy(busy) {
|
|
109
|
+
document.querySelectorAll('.ck-icon-btn[title="Refresh models"]').forEach(function(btn) {
|
|
110
|
+
btn.classList.toggle('ck-icon-btn--spinning', busy);
|
|
111
|
+
btn.disabled = busy;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
108
114
|
function ckRefreshModels() {
|
|
109
115
|
if (ckRefreshing) return;
|
|
110
116
|
ckRefreshing = true;
|
|
111
|
-
|
|
112
|
-
if (btn) btn.classList.add('ck-icon-btn--spinning');
|
|
117
|
+
ckSetRefreshButtonsBusy(true);
|
|
113
118
|
ckUpdateRefreshProgress();
|
|
114
119
|
var csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
|
|
115
120
|
fetch("/completion_kit/refresh_models", {
|
|
@@ -149,8 +154,7 @@ document.addEventListener("turbo:before-stream-render", function(event) {
|
|
|
149
154
|
}
|
|
150
155
|
if (target === "prompt_llm_model" || target === "run_judge_model") {
|
|
151
156
|
ckRefreshing = false;
|
|
152
|
-
|
|
153
|
-
if (btn) btn.classList.remove('ck-icon-btn--spinning');
|
|
157
|
+
ckSetRefreshButtonsBusy(false);
|
|
154
158
|
var status = document.getElementById('refresh-status');
|
|
155
159
|
if (status) { status.textContent = 'Models updated.'; setTimeout(function() { status.textContent = ' '; }, 3000); }
|
|
156
160
|
}
|
|
@@ -1164,6 +1164,17 @@ tr:hover .ck-chip--publish {
|
|
|
1164
1164
|
cursor: help;
|
|
1165
1165
|
}
|
|
1166
1166
|
|
|
1167
|
+
.ck-model-table tr.ck-model-table__section td {
|
|
1168
|
+
padding: 0.85rem 0 0.3rem;
|
|
1169
|
+
font-family: var(--ck-mono);
|
|
1170
|
+
font-size: 0.66rem;
|
|
1171
|
+
font-weight: 600;
|
|
1172
|
+
letter-spacing: 0.16em;
|
|
1173
|
+
text-transform: uppercase;
|
|
1174
|
+
color: var(--ck-dim);
|
|
1175
|
+
}
|
|
1176
|
+
.ck-model-table tr.ck-model-table__section:first-child td { padding-top: 0; }
|
|
1177
|
+
|
|
1167
1178
|
.ck-model-list__summary {
|
|
1168
1179
|
display: flex;
|
|
1169
1180
|
align-items: center;
|
|
@@ -1278,6 +1289,102 @@ tr:hover .ck-chip--publish {
|
|
|
1278
1289
|
max-width: 36rem;
|
|
1279
1290
|
}
|
|
1280
1291
|
|
|
1292
|
+
.ck-version-note {
|
|
1293
|
+
margin: 0 0 1rem;
|
|
1294
|
+
max-width: 36rem;
|
|
1295
|
+
font-family: var(--ck-sans);
|
|
1296
|
+
font-size: 0.85rem;
|
|
1297
|
+
line-height: 1.5;
|
|
1298
|
+
color: var(--ck-muted);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
.ck-version-change {
|
|
1302
|
+
margin: 0.5rem 0 0;
|
|
1303
|
+
display: flex;
|
|
1304
|
+
align-items: center;
|
|
1305
|
+
gap: 0.5rem;
|
|
1306
|
+
flex-wrap: wrap;
|
|
1307
|
+
font-family: var(--ck-mono);
|
|
1308
|
+
font-size: 0.8rem;
|
|
1309
|
+
}
|
|
1310
|
+
.ck-version-change__label {
|
|
1311
|
+
font-size: 0.66rem;
|
|
1312
|
+
font-weight: 600;
|
|
1313
|
+
letter-spacing: 0.12em;
|
|
1314
|
+
text-transform: uppercase;
|
|
1315
|
+
color: var(--ck-dim);
|
|
1316
|
+
}
|
|
1317
|
+
.ck-version-change__old {
|
|
1318
|
+
padding: 0.15em 0.5em;
|
|
1319
|
+
border-radius: var(--ck-radius);
|
|
1320
|
+
background: var(--ck-danger-soft);
|
|
1321
|
+
color: var(--ck-danger);
|
|
1322
|
+
text-decoration: line-through;
|
|
1323
|
+
}
|
|
1324
|
+
.ck-version-change__arrow { color: var(--ck-dim); }
|
|
1325
|
+
.ck-version-change__new {
|
|
1326
|
+
padding: 0.15em 0.5em;
|
|
1327
|
+
border-radius: var(--ck-radius);
|
|
1328
|
+
background: var(--ck-success-soft);
|
|
1329
|
+
color: var(--ck-success);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
.ck-modal__body .ck-version-change + .ck-suggest-diff { margin-top: 0.9rem; }
|
|
1333
|
+
|
|
1334
|
+
.ck-cell-link {
|
|
1335
|
+
background: none;
|
|
1336
|
+
border: none;
|
|
1337
|
+
padding: 0;
|
|
1338
|
+
font-family: var(--ck-mono);
|
|
1339
|
+
font-size: 0.8rem;
|
|
1340
|
+
color: var(--ck-accent);
|
|
1341
|
+
cursor: pointer;
|
|
1342
|
+
text-decoration: underline;
|
|
1343
|
+
text-underline-offset: 2px;
|
|
1344
|
+
}
|
|
1345
|
+
.ck-cell-link:hover { color: var(--ck-accent-hover); }
|
|
1346
|
+
.ck-cell-link--delta {
|
|
1347
|
+
font-size: 1rem;
|
|
1348
|
+
font-weight: 700;
|
|
1349
|
+
text-decoration: none;
|
|
1350
|
+
color: var(--ck-dim);
|
|
1351
|
+
}
|
|
1352
|
+
.ck-cell-link--delta:hover { color: var(--ck-accent); }
|
|
1353
|
+
|
|
1354
|
+
/* Version-history table: cell that pairs the version label + publish control (left) with the diff trigger (right) */
|
|
1355
|
+
.ck-version-cell {
|
|
1356
|
+
display: flex;
|
|
1357
|
+
align-items: center;
|
|
1358
|
+
justify-content: space-between;
|
|
1359
|
+
gap: 0.75rem;
|
|
1360
|
+
}
|
|
1361
|
+
.ck-version-cell__label { display: flex; align-items: center; gap: 0.5rem; }
|
|
1362
|
+
|
|
1363
|
+
/* the version currently being viewed: faint accent wash (like the model chips)
|
|
1364
|
+
plus a bright accent border drawn on the cells (Safari can't box-shadow a <tr>) */
|
|
1365
|
+
.ck-results-table tbody tr.ck-results-table__row--active,
|
|
1366
|
+
.ck-results-table tbody tr.ck-results-table__row--active:hover {
|
|
1367
|
+
background: var(--ck-accent-soft);
|
|
1368
|
+
}
|
|
1369
|
+
.ck-results-table tbody tr.ck-results-table__row--active td {
|
|
1370
|
+
box-shadow: inset 0 2px 0 var(--ck-accent), inset 0 -2px 0 var(--ck-accent);
|
|
1371
|
+
}
|
|
1372
|
+
.ck-results-table tbody tr.ck-results-table__row--active td:first-child {
|
|
1373
|
+
box-shadow: inset 0 2px 0 var(--ck-accent), inset 0 -2px 0 var(--ck-accent), inset 2px 0 0 var(--ck-accent);
|
|
1374
|
+
}
|
|
1375
|
+
.ck-results-table tbody tr.ck-results-table__row--active td:last-child {
|
|
1376
|
+
box-shadow: inset 0 2px 0 var(--ck-accent), inset 0 -2px 0 var(--ck-accent), inset -2px 0 0 var(--ck-accent);
|
|
1377
|
+
}
|
|
1378
|
+
/* when it's the last row, round its bottom corners so the border isn't clipped
|
|
1379
|
+
by the table's rounded container */
|
|
1380
|
+
.ck-results-table tbody tr.ck-results-table__row--active:last-child td:first-child {
|
|
1381
|
+
border-bottom-left-radius: calc(var(--ck-radius-lg) - 1px);
|
|
1382
|
+
}
|
|
1383
|
+
.ck-results-table tbody tr.ck-results-table__row--active:last-child td:last-child {
|
|
1384
|
+
border-bottom-right-radius: calc(var(--ck-radius-lg) - 1px);
|
|
1385
|
+
}
|
|
1386
|
+
.ck-results-table tbody tr.ck-results-table__row--active strong { color: var(--ck-accent-hover); }
|
|
1387
|
+
|
|
1281
1388
|
.ck-field-hint {
|
|
1282
1389
|
font-family: var(--ck-sans);
|
|
1283
1390
|
font-size: 0.8rem;
|
|
@@ -1659,30 +1766,54 @@ tr:hover .ck-chip--publish {
|
|
|
1659
1766
|
color: var(--ck-text);
|
|
1660
1767
|
}
|
|
1661
1768
|
|
|
1769
|
+
@keyframes ck-flash-in {
|
|
1770
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
1771
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1662
1774
|
.ck-flash {
|
|
1663
|
-
margin-bottom:
|
|
1664
|
-
padding: 0.
|
|
1665
|
-
border-radius: var(--ck-radius);
|
|
1775
|
+
margin-bottom: 1.25rem;
|
|
1776
|
+
padding: 0.8rem 1rem;
|
|
1666
1777
|
border: 1px solid transparent;
|
|
1778
|
+
border-radius: var(--ck-radius);
|
|
1667
1779
|
font-family: var(--ck-sans);
|
|
1668
|
-
font-size: 0.
|
|
1780
|
+
font-size: 0.9rem;
|
|
1781
|
+
line-height: 1.6;
|
|
1782
|
+
animation: ck-flash-in 0.22s ease both;
|
|
1669
1783
|
}
|
|
1784
|
+
.ck-flash--notice { background: rgba(45, 212, 168, 0.08); border-color: rgba(45, 212, 168, 0.32); }
|
|
1785
|
+
.ck-flash--alert { background: rgba(248, 113, 113, 0.08); border-color: rgba(248, 113, 113, 0.32); }
|
|
1670
1786
|
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1787
|
+
/* layout flash: a mono status tag (the engine's kicker treatment) inline before the message */
|
|
1788
|
+
.ck-flash__label {
|
|
1789
|
+
margin-right: 0.6rem;
|
|
1790
|
+
font-family: var(--ck-mono);
|
|
1791
|
+
font-size: 0.7rem;
|
|
1792
|
+
font-weight: 600;
|
|
1793
|
+
letter-spacing: 0.14em;
|
|
1794
|
+
text-transform: uppercase;
|
|
1795
|
+
white-space: nowrap;
|
|
1675
1796
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1797
|
+
.ck-flash__label::before {
|
|
1798
|
+
content: "";
|
|
1799
|
+
display: inline-block;
|
|
1800
|
+
width: 6px;
|
|
1801
|
+
height: 6px;
|
|
1802
|
+
margin-right: 0.45rem;
|
|
1803
|
+
border-radius: 50%;
|
|
1804
|
+
background: currentColor;
|
|
1805
|
+
box-shadow: 0 0 7px currentColor;
|
|
1806
|
+
vertical-align: 0.12em;
|
|
1681
1807
|
}
|
|
1808
|
+
.ck-flash--notice .ck-flash__label { color: var(--ck-success); }
|
|
1809
|
+
.ck-flash--alert .ck-flash__label { color: var(--ck-danger); }
|
|
1810
|
+
.ck-flash__body { color: var(--ck-text); }
|
|
1682
1811
|
|
|
1683
|
-
.ck-
|
|
1684
|
-
|
|
1685
|
-
|
|
1812
|
+
/* form-error summary inside .ck-flash--alert: title sentence + ck-error-list */
|
|
1813
|
+
.ck-flash__title { margin: 0; font-weight: 600; color: var(--ck-danger); }
|
|
1814
|
+
|
|
1815
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1816
|
+
.ck-flash { animation: none; }
|
|
1686
1817
|
}
|
|
1687
1818
|
|
|
1688
1819
|
.ck-banner {
|
|
@@ -2012,6 +2143,10 @@ select.ck-input {
|
|
|
2012
2143
|
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55);
|
|
2013
2144
|
overflow: hidden;
|
|
2014
2145
|
}
|
|
2146
|
+
/* the panel is the dialog's autofocus target (so the close button isn't auto-focused);
|
|
2147
|
+
it's a container, not a control, so don't show a ring on it */
|
|
2148
|
+
.ck-modal__panel:focus,
|
|
2149
|
+
.ck-modal__panel:focus-visible { outline: none; }
|
|
2015
2150
|
|
|
2016
2151
|
.ck-modal__header {
|
|
2017
2152
|
display: flex;
|
|
@@ -2048,9 +2183,8 @@ select.ck-input {
|
|
|
2048
2183
|
|
|
2049
2184
|
.ck-modal__close {
|
|
2050
2185
|
appearance: none;
|
|
2186
|
+
position: relative;
|
|
2051
2187
|
display: inline-flex;
|
|
2052
|
-
align-items: center;
|
|
2053
|
-
justify-content: center;
|
|
2054
2188
|
width: 28px;
|
|
2055
2189
|
height: 28px;
|
|
2056
2190
|
padding: 0;
|
|
@@ -2059,12 +2193,25 @@ select.ck-input {
|
|
|
2059
2193
|
outline: none;
|
|
2060
2194
|
background: transparent;
|
|
2061
2195
|
color: var(--ck-dim);
|
|
2062
|
-
font-
|
|
2063
|
-
font-size: 1.5rem;
|
|
2064
|
-
line-height: 1;
|
|
2196
|
+
font-size: 0;
|
|
2065
2197
|
cursor: pointer;
|
|
2066
2198
|
transition: color 0.15s, background 0.15s;
|
|
2067
2199
|
}
|
|
2200
|
+
/* the "×" is drawn as two crossed bars centred on the button, so it (and the
|
|
2201
|
+
focus ring around the button) sit dead centre regardless of font metrics */
|
|
2202
|
+
.ck-modal__close::before,
|
|
2203
|
+
.ck-modal__close::after {
|
|
2204
|
+
content: "";
|
|
2205
|
+
position: absolute;
|
|
2206
|
+
top: 50%;
|
|
2207
|
+
left: 50%;
|
|
2208
|
+
width: 13px;
|
|
2209
|
+
height: 1.6px;
|
|
2210
|
+
border-radius: 1px;
|
|
2211
|
+
background: currentColor;
|
|
2212
|
+
}
|
|
2213
|
+
.ck-modal__close::before { transform: translate(-50%, -50%) rotate(45deg); }
|
|
2214
|
+
.ck-modal__close::after { transform: translate(-50%, -50%) rotate(-45deg); }
|
|
2068
2215
|
|
|
2069
2216
|
.ck-modal__close:hover {
|
|
2070
2217
|
color: var(--ck-text);
|
|
@@ -2083,6 +2230,8 @@ select.ck-input {
|
|
|
2083
2230
|
overflow: auto;
|
|
2084
2231
|
padding: 0 1.5rem;
|
|
2085
2232
|
}
|
|
2233
|
+
/* no footer? then the body needs its own bottom padding */
|
|
2234
|
+
.ck-modal__body:last-child { padding-bottom: 1.5rem; }
|
|
2086
2235
|
|
|
2087
2236
|
.ck-modal__footer {
|
|
2088
2237
|
display: flex;
|
|
@@ -2603,9 +2752,15 @@ select.ck-input {
|
|
|
2603
2752
|
}
|
|
2604
2753
|
}
|
|
2605
2754
|
|
|
2755
|
+
/* the metrics field stacks several sub-sections (hint, groups, divider, tag
|
|
2756
|
+
filter, checkboxes) — give it more vertical breathing room than a plain field */
|
|
2757
|
+
#metrics-field {
|
|
2758
|
+
gap: 0.85rem;
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2606
2761
|
.ck-metric-groups {
|
|
2607
2762
|
display: grid;
|
|
2608
|
-
gap: 0.
|
|
2763
|
+
gap: 0.55rem;
|
|
2609
2764
|
margin-bottom: 0.5rem;
|
|
2610
2765
|
}
|
|
2611
2766
|
|
|
@@ -2621,7 +2776,7 @@ select.ck-input {
|
|
|
2621
2776
|
.ck-metric-groups__row {
|
|
2622
2777
|
display: flex;
|
|
2623
2778
|
flex-wrap: wrap;
|
|
2624
|
-
gap: 0.
|
|
2779
|
+
gap: 0.5rem;
|
|
2625
2780
|
}
|
|
2626
2781
|
|
|
2627
2782
|
.ck-metric-group-pill {
|
|
@@ -2878,6 +3033,29 @@ select.ck-input {
|
|
|
2878
3033
|
color: var(--ck-dim);
|
|
2879
3034
|
}
|
|
2880
3035
|
|
|
3036
|
+
.ck-prompts-table__desc {
|
|
3037
|
+
margin: 0.3rem 0 0;
|
|
3038
|
+
font-family: var(--ck-sans);
|
|
3039
|
+
font-size: 0.82rem;
|
|
3040
|
+
font-weight: 400;
|
|
3041
|
+
line-height: 1.45;
|
|
3042
|
+
color: var(--ck-muted);
|
|
3043
|
+
white-space: normal;
|
|
3044
|
+
max-width: 42ch;
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
.ck-endpoint--compact {
|
|
3048
|
+
margin-top: 0.45rem;
|
|
3049
|
+
max-width: 26rem;
|
|
3050
|
+
}
|
|
3051
|
+
.ck-endpoint--compact .ck-endpoint__url {
|
|
3052
|
+
min-width: 0;
|
|
3053
|
+
white-space: nowrap;
|
|
3054
|
+
overflow: hidden;
|
|
3055
|
+
text-overflow: ellipsis;
|
|
3056
|
+
}
|
|
3057
|
+
.ck-endpoint--compact .ck-icon-btn { flex-shrink: 0; }
|
|
3058
|
+
|
|
2881
3059
|
.ck-clamp-2 {
|
|
2882
3060
|
display: -webkit-box;
|
|
2883
3061
|
-webkit-line-clamp: 2;
|
|
@@ -50,7 +50,7 @@ module CompletionKit
|
|
|
50
50
|
|
|
51
51
|
def publish
|
|
52
52
|
@prompt.publish!
|
|
53
|
-
redirect_to prompt_path(@prompt), notice: "#{@prompt.display_name} is now the
|
|
53
|
+
redirect_to prompt_path(@prompt), notice: "#{@prompt.display_name} is now the published version."
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
private
|
|
@@ -28,6 +28,11 @@ module CompletionKit
|
|
|
28
28
|
|
|
29
29
|
def new
|
|
30
30
|
@run = Run.new(prompt_id: params[:prompt_id])
|
|
31
|
+
prompt = Prompt.find_by(id: @run.prompt_id)
|
|
32
|
+
if prompt
|
|
33
|
+
last_run = Run.where(prompt_id: prompt.family_versions.ids).order(created_at: :desc).first
|
|
34
|
+
@run.tag_names = last_run.tag_names if last_run
|
|
35
|
+
end
|
|
31
36
|
end
|
|
32
37
|
|
|
33
38
|
def edit
|
|
@@ -79,6 +84,7 @@ module CompletionKit
|
|
|
79
84
|
dataset_id: @run.dataset_id,
|
|
80
85
|
judge_model: @run.judge_model,
|
|
81
86
|
temperature: @run.temperature,
|
|
87
|
+
tag_names: @run.tag_names,
|
|
82
88
|
status: "pending"
|
|
83
89
|
)
|
|
84
90
|
new_run.replace_metrics!(@run.metric_ids)
|
|
@@ -72,6 +72,47 @@ module CompletionKit
|
|
|
72
72
|
CompletionKit::ProviderCredential::PROVIDER_LABELS[provider.to_s] || provider.to_s.titleize
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
def ck_masked_token(token)
|
|
76
|
+
return "YOUR_TOKEN" if token.blank?
|
|
77
|
+
return "••••••••" if token.length < 12
|
|
78
|
+
"#{token[0..3]}#{'•' * [token.length - 8, 4].max}#{token[-4..]}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
OPENAI_MODEL_FAMILY_ORDER = ["GPT-5", "GPT-4", "o-series", "GPT-3.5", "GPT-OSS", "Other"].freeze
|
|
82
|
+
|
|
83
|
+
def ck_openai_model_family(model_id)
|
|
84
|
+
id = model_id.to_s
|
|
85
|
+
return "GPT-5" if id.match?(/\Agpt-5/i)
|
|
86
|
+
return "GPT-4" if id.match?(/\Agpt-4/i)
|
|
87
|
+
return "GPT-3.5" if id.match?(/\Agpt-3/i)
|
|
88
|
+
return "GPT-OSS" if id.match?(/\Agpt-oss/i)
|
|
89
|
+
return "o-series" if id.match?(/\Ao\d/i)
|
|
90
|
+
"Other"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Groups a provider's models for the models-card table, mirroring how the
|
|
94
|
+
# dropdown sub-groups: OpenRouter clusters by upstream vendor (the part
|
|
95
|
+
# before "/"); OpenAI clusters by family; everyone else stays flat. Returns
|
|
96
|
+
# [[section_label_or_nil, [models]], ...]. A single section collapses to a
|
|
97
|
+
# nil label so we don't render a redundant header.
|
|
98
|
+
def ck_model_table_sections(models)
|
|
99
|
+
models = models.to_a
|
|
100
|
+
sections =
|
|
101
|
+
case models.first&.provider
|
|
102
|
+
when "openrouter"
|
|
103
|
+
models.group_by { |m| m.model_id.to_s.split("/", 2).first.delete_prefix("~") }
|
|
104
|
+
.sort_by { |label, _| label }
|
|
105
|
+
when "openai"
|
|
106
|
+
grouped = models.group_by { |m| ck_openai_model_family(m.model_id) }
|
|
107
|
+
ordered = OPENAI_MODEL_FAMILY_ORDER.filter_map { |label| [label, grouped[label]] if grouped[label] }
|
|
108
|
+
extras = (grouped.keys - OPENAI_MODEL_FAMILY_ORDER).sort.map { |label| [label, grouped[label]] }
|
|
109
|
+
ordered + extras
|
|
110
|
+
else
|
|
111
|
+
[[nil, models]]
|
|
112
|
+
end
|
|
113
|
+
sections.size <= 1 ? [[nil, models]] : sections
|
|
114
|
+
end
|
|
115
|
+
|
|
75
116
|
def ck_model_option_label(model)
|
|
76
117
|
return "#{model[:name]} (?)" if model.key?(:judging_confirmed) && !model[:judging_confirmed]
|
|
77
118
|
model[:name]
|
|
@@ -85,20 +126,33 @@ module CompletionKit
|
|
|
85
126
|
end
|
|
86
127
|
end
|
|
87
128
|
|
|
88
|
-
groups = models.group_by
|
|
89
|
-
|
|
90
|
-
upstream = m[:id].to_s.split("/", 2).first
|
|
91
|
-
"OpenRouter — #{upstream}"
|
|
92
|
-
else
|
|
93
|
-
ck_provider_label(m[:provider])
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
ordered_keys = groups.keys.sort_by { |label| [label.start_with?("OpenRouter") ? 1 : 0, label] }
|
|
129
|
+
groups = models.group_by { |m| ck_model_optgroup_label(m) }
|
|
130
|
+
ordered_keys = groups.keys.sort_by { |label| ck_model_optgroup_sort_key(label) }
|
|
98
131
|
grouped = ordered_keys.map { |label| [label, groups[label].map { |m| [ck_model_option_label(m), m[:id]] }] }
|
|
99
132
|
grouped_options_for_select(grouped, selected)
|
|
100
133
|
end
|
|
101
134
|
|
|
135
|
+
# Optgroup label for the model select — mirrors the provider models table:
|
|
136
|
+
# OpenRouter splits by upstream vendor, OpenAI splits by family, everyone
|
|
137
|
+
# else is a single group.
|
|
138
|
+
def ck_model_optgroup_label(model)
|
|
139
|
+
case model[:provider]
|
|
140
|
+
when "openrouter" then "OpenRouter — #{model[:id].to_s.split("/", 2).first.delete_prefix("~")}"
|
|
141
|
+
when "openai" then "OpenAI — #{ck_openai_model_family(model[:id])}"
|
|
142
|
+
else ck_provider_label(model[:provider])
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def ck_model_optgroup_sort_key(label)
|
|
147
|
+
if label.start_with?("OpenAI — ")
|
|
148
|
+
[0, OPENAI_MODEL_FAMILY_ORDER.index(label.delete_prefix("OpenAI — ")), label]
|
|
149
|
+
elsif label.start_with?("OpenRouter")
|
|
150
|
+
[2, 0, label]
|
|
151
|
+
else
|
|
152
|
+
[1, 0, label]
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
102
156
|
def ck_model_options_html(scope)
|
|
103
157
|
models = CompletionKit::ApiConfig.available_models(scope: scope)
|
|
104
158
|
return "" if models.empty?
|
|
@@ -47,6 +47,14 @@ module CompletionKit
|
|
|
47
47
|
false
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
def model_count
|
|
51
|
+
Model.where(provider: provider).active.count
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.discovery_in_progress?
|
|
55
|
+
where(discovery_status: "discovering").exists?
|
|
56
|
+
end
|
|
57
|
+
|
|
50
58
|
def prompt_count
|
|
51
59
|
model_ids = Model.where(provider: provider).pluck(:model_id)
|
|
52
60
|
return 0 if model_ids.empty?
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
module CompletionKit
|
|
2
2
|
class WorkerHealth
|
|
3
|
-
|
|
3
|
+
# SolidQueue workers heartbeat every SolidQueue.process_heartbeat_interval
|
|
4
|
+
# (60s by default), so a 30s window flagged healthy workers as down between
|
|
5
|
+
# beats. Allow two missed heartbeats before we say the worker is gone.
|
|
6
|
+
HEARTBEAT_THRESHOLD = 2.minutes
|
|
4
7
|
|
|
5
8
|
def self.healthy?
|
|
6
9
|
return true unless defined?(::SolidQueue::Process)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<% if token.present? %>
|
|
2
|
+
<% masked = ck_masked_token(token) %>
|
|
3
|
+
<div style="display: flex; align-items: center; gap: 0.4rem; margin: 0.5rem 0;">
|
|
4
|
+
<button type="button" class="ck-chip ck-token-toggle" onclick="ckToggleToken(this)" data-masked="<%= masked %>" data-real="<%= token %>" aria-label="Reveal API token" aria-pressed="false" style="cursor: pointer;"><%= masked %></button>
|
|
5
|
+
<button type="button" class="ck-icon-btn" title="Copy token" aria-label="Copy API token" onclick="var b=this;navigator.clipboard.writeText('<%= token %>').then(function(){b.classList.add('ck-api-copy--done');setTimeout(function(){b.classList.remove('ck-api-copy--done')},1500)})">
|
|
6
|
+
<%= heroicon_tag "clipboard-document", variant: :outline, size: 14, "aria-hidden": "true" %>
|
|
7
|
+
</button>
|
|
8
|
+
</div>
|
|
9
|
+
<% else %>
|
|
10
|
+
<p class="ck-meta-copy">No API token configured. Set <code>COMPLETION_KIT_API_TOKEN</code> to enable API access.</p>
|
|
11
|
+
<% end %>
|